Spotify History storing from CLI and inclusion in the front end #50

Merged
Sarsoo merged 3 commits from spotifyhistory into master 2022-10-08 19:59:35 +01:00
21 changed files with 761 additions and 42 deletions
Showing only changes of commit 75415b4db4 - Show all commits

View File

@ -52,7 +52,6 @@ namespace Selector.CLI
var directoryContents = Directory.EnumerateFiles(path); var directoryContents = Directory.EnumerateFiles(path);
var endSongs = directoryContents.Where(f => f.Contains("endsong_")).ToArray(); var endSongs = directoryContents.Where(f => f.Contains("endsong_")).ToArray();
foreach(var file in endSongs) foreach(var file in endSongs)
{ {
streams.Add(File.OpenRead(file)); streams.Add(File.OpenRead(file));

View File

@ -118,8 +118,12 @@ namespace Selector.CLI.Extensions
options.UseNpgsql(config.DatabaseOptions.ConnectionString) options.UseNpgsql(config.DatabaseOptions.ConnectionString)
); );
services.AddTransient<IScrobbleRepository, ScrobbleRepository>(); services.AddTransient<IScrobbleRepository, ScrobbleRepository>()
services.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>(); .AddTransient<ISpotifyListenRepository, SpotifyListenRepository>();
//services.AddTransient<IListenRepository, MetaListenRepository>();
services.AddTransient<IListenRepository, SpotifyListenRepository>();
services.AddTransient<IScrobbleMappingRepository, ScrobbleMappingRepository>(); services.AddTransient<IScrobbleMappingRepository, ScrobbleMappingRepository>();
services.AddHostedService<MigratorService>(); services.AddHostedService<MigratorService>();

View File

@ -112,7 +112,7 @@ namespace Selector
logger.LogDebug("Identifying difference sets"); logger.LogDebug("Identifying difference sets");
var time = Stopwatch.StartNew(); var time = Stopwatch.StartNew();
(var toAdd, var toRemove) = ScrobbleMatcher.IdentifyDiffs(currentScrobbles, nativeScrobbles); (var toAdd, var toRemove) = ListenMatcher.IdentifyDiffs(currentScrobbles, nativeScrobbles);
time.Stop(); time.Stop();
logger.LogTrace("Finished diffing: {:n}ms", time.ElapsedMilliseconds); logger.LogTrace("Finished diffing: {:n}ms", time.ElapsedMilliseconds);

View File

@ -50,7 +50,7 @@ public class HistoryPersister
var parsed = await JsonSerializer.DeserializeAsync(singleInput, Json.EndSongArray); var parsed = await JsonSerializer.DeserializeAsync(singleInput, Json.EndSongArray);
songs = songs.Concat(parsed); 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); await Process(songs);
@ -67,7 +67,13 @@ public class HistoryPersister
var counter = 0; 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)) if(!string.IsNullOrWhiteSpace(item.master_metadata_track_name))
{ {

View File

@ -87,7 +87,12 @@ namespace Selector.Model
.Property(s => s.LastfmArtistName) .Property(s => s.LastfmArtistName)
.UseCollation("case_insensitive"); .UseCollation("case_insensitive");
modelBuilder.Entity<SpotifyListen>().HasKey(s => s.Timestamp); modelBuilder.Entity<ArtistLastfmSpotifyMapping>().HasKey(s => s.SpotifyUri);
modelBuilder.Entity<ArtistLastfmSpotifyMapping>()
.Property(s => s.LastfmArtistName)
.UseCollation("case_insensitive");
modelBuilder.Entity<SpotifyListen>().HasKey(s => s.Id);
modelBuilder.Entity<SpotifyListen>() modelBuilder.Entity<SpotifyListen>()
.Property(s => s.TrackName) .Property(s => s.TrackName)
.UseCollation("case_insensitive"); .UseCollation("case_insensitive");

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Selector.Model
{
public interface IListenRepository
{
IEnumerable<IListen> 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);
}
}

View File

@ -4,18 +4,18 @@ using System.Threading.Tasks;
namespace Selector.Model namespace Selector.Model
{ {
public interface ISpotifyListenRepository public interface ISpotifyListenRepository: IListenRepository
{ {
void Add(SpotifyListen item); void Add(SpotifyListen item);
void AddRange(IEnumerable<SpotifyListen> item); void AddRange(IEnumerable<SpotifyListen> item);
IEnumerable<SpotifyListen> 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<SpotifyListen> 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); SpotifyListen Find(DateTime key, string include = null);
void Remove(DateTime key); void Remove(DateTime key);
public void Remove(SpotifyListen scrobble); public void Remove(SpotifyListen scrobble);
public void RemoveRange(IEnumerable<SpotifyListen> scrobbles); public void RemoveRange(IEnumerable<SpotifyListen> scrobbles);
void Update(SpotifyListen item); void Update(SpotifyListen item);
Task<int> Save(); Task<int> 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);
} }
} }

View File

@ -0,0 +1,10 @@
using System;
namespace Selector.Model;
public interface IUserListen: IListen
{
string UserId { get; set; }
ApplicationUser User { get; set; }
}

View File

@ -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<IListen> 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;
}
}

View File

@ -2,8 +2,10 @@
namespace Selector.Model; namespace Selector.Model;
public class SpotifyListen: Listen public class SpotifyListen: Listen, IUserListen
{ {
public int Id { get; set; }
public int? PlayedDuration { get; set; } public int? PlayedDuration { get; set; }
public string TrackUri { get; set; } public string TrackUri { get; set; }

View File

@ -95,7 +95,7 @@ namespace Selector.Model
return listens; return listens;
} }
public IEnumerable<SpotifyListen> 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<IListen> 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(); => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).AsEnumerable();
public void Remove(DateTime key) public void Remove(DateTime key)
@ -123,7 +123,7 @@ namespace Selector.Model
return db.SaveChangesAsync(); 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) 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(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); => GetAllQueryable(userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count();
} }
} }

View File

@ -0,0 +1,491 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Selector.Model.AlbumLastfmSpotifyMapping", b =>
{
b.Property<string>("SpotifyUri")
.HasColumnType("text");
b.Property<string>("LastfmAlbumName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("LastfmArtistName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.HasKey("SpotifyUri");
b.ToTable("AlbumMapping");
});
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<string>("LastFmUsername")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<bool>("SaveScrobbles")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<string>("SpotifyAccessToken")
.HasColumnType("text");
b.Property<bool>("SpotifyIsLinked")
.HasColumnType("boolean");
b.Property<DateTime>("SpotifyLastRefresh")
.HasColumnType("timestamp with time zone");
b.Property<string>("SpotifyRefreshToken")
.HasColumnType("text");
b.Property<int>("SpotifyTokenExpiry")
.HasColumnType("integer");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("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<string>("SpotifyUri")
.HasColumnType("text");
b.Property<string>("LastfmArtistName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.HasKey("SpotifyUri");
b.ToTable("ArtistMapping");
});
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AlbumName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("ArtistName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<int?>("PlayedDuration")
.HasColumnType("integer");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("TrackName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("TrackUri")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpotifyListen");
});
modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b =>
{
b.Property<string>("SpotifyUri")
.HasColumnType("text");
b.Property<string>("LastfmArtistName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("LastfmTrackName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.HasKey("SpotifyUri");
b.ToTable("TrackMapping");
});
modelBuilder.Entity("Selector.Model.UserScrobble", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AlbumArtistName")
.HasColumnType("text");
b.Property<string>("AlbumName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("ArtistName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("TrackName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Scrobble");
});
modelBuilder.Entity("Selector.Model.Watcher", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Watcher");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Selector.Model.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Selector.Model.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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
}
}
}

View File

@ -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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PlayedDuration = table.Column<int>(type: "integer", nullable: true),
TrackUri = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: true),
TrackName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"),
AlbumName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"),
ArtistName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"),
Timestamp = table.Column<DateTime>(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");
}
}
}

View File

@ -18,7 +18,7 @@ namespace Selector.Model.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "en-u-ks-primary,en-u-ks-primary,icu,False") .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); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -52,7 +52,7 @@ namespace Selector.Model.Migrations
new new
{ {
Id = "00c64c0a-3387-4933-9575-83443fa9092b", Id = "00c64c0a-3387-4933-9575-83443fa9092b",
ConcurrencyStamp = "ec454f56-2b26-4bd8-be8e-a7fd34981ac2", ConcurrencyStamp = "4b4a37c7-cc65-485a-ac0e-d88ef6dede78",
Name = "Admin", Name = "Admin",
NormalizedName = "ADMIN" NormalizedName = "ADMIN"
}); });
@ -282,6 +282,45 @@ namespace Selector.Model.Migrations
b.ToTable("ArtistMapping"); b.ToTable("ArtistMapping");
}); });
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AlbumName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("ArtistName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<int?>("PlayedDuration")
.HasColumnType("integer");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("TrackName")
.HasColumnType("text")
.UseCollation("case_insensitive");
b.Property<string>("TrackUri")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpotifyListen");
});
modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b => modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b =>
{ {
b.Property<string>("SpotifyUri") b.Property<string>("SpotifyUri")
@ -409,6 +448,15 @@ namespace Selector.Model.Migrations
.IsRequired(); .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 => modelBuilder.Entity("Selector.Model.UserScrobble", b =>
{ {
b.HasOne("Selector.Model.ApplicationUser", "User") b.HasOne("Selector.Model.ApplicationUser", "User")

View File

@ -9,12 +9,12 @@ namespace Selector.Cache
{ {
public class DBPlayCountPuller public class DBPlayCountPuller
{ {
protected readonly IScrobbleRepository ScrobbleRepository; protected readonly IListenRepository ScrobbleRepository;
private readonly IOptions<NowPlayingOptions> nowOptions; private readonly IOptions<NowPlayingOptions> nowOptions;
public DBPlayCountPuller( public DBPlayCountPuller(
IOptions<NowPlayingOptions> options, IOptions<NowPlayingOptions> options,
IScrobbleRepository scrobbleRepository IListenRepository scrobbleRepository
) )
{ {
ScrobbleRepository = scrobbleRepository; ScrobbleRepository = scrobbleRepository;

View File

@ -6,17 +6,17 @@ using System.Threading.Tasks;
namespace Selector.Model namespace Selector.Model
{ {
public interface IScrobbleRepository public interface IScrobbleRepository: IListenRepository
{ {
void Add(UserScrobble item); void Add(UserScrobble item);
void AddRange(IEnumerable<UserScrobble> item); void AddRange(IEnumerable<UserScrobble> item);
IEnumerable<UserScrobble> 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<UserScrobble> 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); UserScrobble Find(int key, string include = null);
void Remove(int key); void Remove(int key);
public void Remove(UserScrobble scrobble); public void Remove(UserScrobble scrobble);
public void RemoveRange(IEnumerable<UserScrobble> scrobbles); public void RemoveRange(IEnumerable<UserScrobble> scrobbles);
void Update(UserScrobble item); void Update(UserScrobble item);
Task<int> Save(); Task<int> 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);
} }
} }

View File

@ -95,7 +95,7 @@ namespace Selector.Model
return scrobbles; return scrobbles;
} }
public IEnumerable<UserScrobble> 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<IListen> 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(); => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).AsEnumerable();
public void Remove(int key) public void Remove(int key)
@ -123,7 +123,7 @@ namespace Selector.Model
return db.SaveChangesAsync(); 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) 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(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); => GetAllQueryable(userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count();
} }
} }

View File

@ -2,7 +2,7 @@
namespace Selector.Model namespace Selector.Model
{ {
public class UserScrobble: Scrobble public class UserScrobble: Scrobble, IUserListen
{ {
public int Id { get; set; } public int Id { get; set; }
public string UserId { get; set; } public string UserId { get; set; }

View File

@ -64,7 +64,11 @@ namespace Selector.Web
options.UseNpgsql(Configuration.GetConnectionString("Default")) options.UseNpgsql(Configuration.GetConnectionString("Default"))
); );
services.AddDBPlayCountPuller(); services.AddDBPlayCountPuller();
services.AddTransient<IScrobbleRepository, ScrobbleRepository>(); services.AddTransient<IScrobbleRepository, ScrobbleRepository>()
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>();
//services.AddTransient<IListenRepository, MetaListenRepository>();
services.AddTransient<IListenRepository, SpotifyListenRepository>();
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()

View File

@ -6,9 +6,9 @@ namespace Selector
{ {
public static class PlayDensity public static class PlayDensity
{ {
public static decimal Density(this IEnumerable<Scrobble> scrobbles, TimeSpan window) => scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow); public static decimal Density(this IEnumerable<IListen> scrobbles, TimeSpan window) => scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow);
public static decimal Density(this IEnumerable<Scrobble> scrobbles, DateTime from, DateTime to) public static decimal Density(this IEnumerable<IListen> scrobbles, DateTime from, DateTime to)
{ {
var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to); var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to);
@ -17,7 +17,7 @@ namespace Selector
return filteredScrobbles.Count() / dayDelta; return filteredScrobbles.Count() / dayDelta;
} }
public static decimal Density(this IEnumerable<Scrobble> scrobbles) public static decimal Density(this IEnumerable<IListen> scrobbles)
{ {
var minDate = scrobbles.Select(s => s.Timestamp).Min(); var minDate = scrobbles.Select(s => s.Timestamp).Min();
var maxDate = scrobbles.Select(s => s.Timestamp).Max(); var maxDate = scrobbles.Select(s => s.Timestamp).Max();

View File

@ -6,22 +6,22 @@ using System.Linq;
namespace Selector 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); => 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); => serviceScrobble.Timestamp.Equals(nativeScrobble.Timestamp);
public static (IEnumerable<Scrobble>, IEnumerable<Scrobble>) IdentifyDiffs(IEnumerable<Scrobble> existing, IEnumerable<Scrobble> toApply, bool matchContents = true) public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffs(IEnumerable<IListen> existing, IEnumerable<IListen> toApply, bool matchContents = true)
{ {
existing = existing.OrderBy(s => s.Timestamp); existing = existing.OrderBy(s => s.Timestamp);
toApply = toApply.OrderBy(s => s.Timestamp); toApply = toApply.OrderBy(s => s.Timestamp);
var toApplyIter = toApply.GetEnumerator(); var toApplyIter = toApply.GetEnumerator();
var toAdd = new List<Scrobble>(); var toAdd = new List<IListen>();
var toRemove = new List<Scrobble>(); var toRemove = new List<IListen>();
var toApplyOverrun = false; var toApplyOverrun = false;
@ -79,7 +79,7 @@ namespace Selector
return (toAdd, toRemove); 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)) if (!currentExisting.TrackName.Equals(toApply.TrackName, StringComparison.InvariantCultureIgnoreCase))
{ {
@ -97,19 +97,19 @@ namespace Selector
} }
} }
public static (IEnumerable<Scrobble>, IEnumerable<Scrobble>) IdentifyDiffsContains(IEnumerable<Scrobble> existing, IEnumerable<Scrobble> toApply) public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffsContains(IEnumerable<IListen> existing, IEnumerable<IListen> toApply)
{ {
var toAdd = toApply.Where(s => !existing.Contains(s, new ScrobbleComp())); var toAdd = toApply.Where(s => !existing.Contains(s, new ListenComp()));
var toRemove = existing.Where(s => !toApply.Contains(s, new ScrobbleComp())); var toRemove = existing.Where(s => !toApply.Contains(s, new ListenComp()));
return (toAdd, toRemove); return (toAdd, toRemove);
} }
public class ScrobbleComp : IEqualityComparer<Scrobble> public class ListenComp : IEqualityComparer<IListen>
{ {
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();
} }
} }
} }