diff --git a/Console/Audio/PlayerController.cs b/Console/Audio/PlayerController.cs index 17a0076..245af8c 100644 --- a/Console/Audio/PlayerController.cs +++ b/Console/Audio/PlayerController.cs @@ -15,7 +15,7 @@ internal class PlayerController(YoutubeClient youtubeClient, SettingsRepository private readonly AsyncLock _lock = new(); private readonly YoutubeClient _youtubeClient = youtubeClient; private readonly SettingsRepository _settingsRepository = settingsRepository; - private float _volume = settingsRepository.GetSettings().Volume / 100f; + private float _volume = -1f; private PlayState _state = PlayState.Stopped; private List _queue = []; private int _currentSongIndex = 0; @@ -41,7 +41,14 @@ public PlayState State } public int Volume { - get { return (int)(_volume * 100); } + get + { + //Load volume if it wasnt loaded before + if (_volume < 0) + _volume = _settingsRepository.GetSettings().Volume / 100f; + + return (int)(_volume * 100); + } set { if (value is < 0 or > 100) @@ -104,7 +111,7 @@ public async ValueTask DisposeAsync() await _audioSender.DisposeAsync().ConfigureAwait(false); } - await _settingsRepository.DisposeAsync(); + _settingsRepository.Dispose(); // Suppress finalization GC.SuppressFinalize(this); diff --git a/Console/Commands/MainCommand.cs b/Console/Commands/MainCommand.cs index 2a1012a..2ad06b9 100644 --- a/Console/Commands/MainCommand.cs +++ b/Console/Commands/MainCommand.cs @@ -1,12 +1,15 @@ -using CliFx; +using System.Data; +using System.Reflection; +using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; using Console.Audio; using Console.Cookies; -using Console.Database; using Console.Extensions; using Console.Repositories; using Console.Views; +using DbUp; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.DependencyInjection; using Terminal.Gui; using YoutubeExplode; @@ -24,9 +27,19 @@ internal class MainCommand : ICommand public async ValueTask ExecuteAsync(IConsole console) { - await Utils.ConfigurePlatformDependenciesAsync(console); + const string connectionString = "Data Source=data.db"; - Application.Init(); + Utils.ConfigurePlatformDependencies(); + await console.Output.WriteLineAsync("Ignore any warnings above this message"); + await console.Output.WriteLineAsync("[PortAudio] Initialized corretly"); + + var upgrader = DeployChanges + .To.SQLiteDatabase(connectionString) + .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) + .LogToConsole() + .Build(); + + var result = upgrader.PerformUpgrade(); var top = new Toplevel(); @@ -104,7 +117,7 @@ public async ValueTask ExecuteAsync(IConsole console) videosWin.Add(tabView); var serviceProvider = new ServiceCollection() - .AddScoped() + .AddScoped(_ => new SqliteConnection(connectionString)) .AddScoped() .AddScoped() .AddSingleton() @@ -190,12 +203,14 @@ public async ValueTask ExecuteAsync(IConsole console) }) .BuildServiceProvider(); - using var dbContext = serviceProvider.GetRequiredService(); + var statusBarFactory = serviceProvider.GetRequiredService(); using var settingsRepository = serviceProvider.GetRequiredService(); - //TODO change to dapper and use fluent migrations cause this will never work - await dbContext.MigrateAOTAsync("migrations.sql"); await settingsRepository.InitializeAsync(); + top.Add(queueWin, searchWin, videosWin, playerWin, statusBarFactory.Create()); + + Application.Init(); + await using var playerController = serviceProvider.GetRequiredService(); var playerView = serviceProvider.GetRequiredService(); var queueView = serviceProvider.GetRequiredService(); @@ -203,16 +218,12 @@ public async ValueTask ExecuteAsync(IConsole console) var recommendationsView = serviceProvider.GetRequiredService(); var videoSearchView = serviceProvider.GetRequiredService(); var localPlaylistsView = serviceProvider.GetRequiredService(); - var statusBarFactory = serviceProvider.GetRequiredService(); videoSearchView.ShowSearch(); playerView.ShowPlayer(); queueView.ShowQueue(); recommendationsView.ShowRecommendations(); localPlaylistsView.ShowLocalPlaylists(); - var statusBar = statusBarFactory.Create(); - - top.Add(queueWin, searchWin, videosWin, playerWin, statusBar); Application.Run(top); top.Dispose(); diff --git a/Console/Console.csproj b/Console/Console.csproj index a5241ae..07adbe9 100644 --- a/Console/Console.csproj +++ b/Console/Console.csproj @@ -7,16 +7,21 @@ enable enable true + $(InterceptorsPreviewNamespaces);Dapper.AOT + + + - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + @@ -29,6 +34,11 @@ + + + + + $(RuntimeIdentifier) @@ -41,20 +51,10 @@ osx-arm64 osx-x64 portaudio.dll - libportaudio.so - libportaudio.dylib + libportaudio.so + libportaudio.a + libportaudio.dylib - - - - - - - - - - - diff --git a/Console/Database/LocalPlaylist.cs b/Console/Database/LocalPlaylist.cs index 626c788..84a77fe 100644 --- a/Console/Database/LocalPlaylist.cs +++ b/Console/Database/LocalPlaylist.cs @@ -1,14 +1,15 @@ -using Microsoft.EntityFrameworkCore; +using Dapper.Contrib.Extensions; namespace Console.Database; -[PrimaryKey("PlaylistId")] internal class LocalPlaylist { + [Key] public int PlaylistId { get; set; } public required string Name { get; set; } // Navigation property for the many-to-many relationship + [Computed] public ICollection PlaylistSongs { get; set; } = []; public override string ToString() => Name ?? ""; diff --git a/Console/Database/LocalSong.cs b/Console/Database/LocalSong.cs index eeb6eaf..927fb39 100644 --- a/Console/Database/LocalSong.cs +++ b/Console/Database/LocalSong.cs @@ -1,24 +1,36 @@ -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; +using Dapper.Contrib.Extensions; +using Lazy; using YoutubeExplode.Common; using YoutubeExplode.Videos; namespace Console.Database; -[PrimaryKey("Id")] internal class LocalSong : IVideo { + [Key] public required string Id { get; set; } public required string Url { get; set; } public required string Title { get; set; } public required string ChannelId { get; set; } public required string ChannelTitle { get; set; } - public required TimeSpan? Duration { get; set; } + public required double DurationMiliseconds { get; set; } - [NotMapped] + [Computed] + [Lazy] + public TimeSpan? Duration => TimeSpan.FromMilliseconds(DurationMiliseconds); + + [Computed] + //Not saved for now, only to satisfy IVideo public IReadOnlyList Thumbnails { get; } = []; + + [Computed] public ICollection? PlaylistSongs { get; set; } + [Computed] + [Lazy] VideoId IVideo.Id => new(Id); + + [Computed] + [Lazy] Author IVideo.Author => new(ChannelId, ChannelTitle); } diff --git a/Console/Database/MyDbContext.cs b/Console/Database/MyDbContext.cs deleted file mode 100644 index 1145af2..0000000 --- a/Console/Database/MyDbContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Console.Database; - -internal class MyDbContext : DbContext -{ - public MyDbContext(DbContextOptions options) - : base(options) { } - - public MyDbContext() { } - - public DbSet Songs { get; set; } - public DbSet Playlists { get; set; } - public DbSet PlaylistSongs { get; set; } - public DbSet Settings { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - optionsBuilder.UseSqlite("Data Source=data.db"); - } - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasKey(ps => new { ps.PlaylistId, ps.SongId }); - - modelBuilder - .Entity() - .HasOne(ps => ps.Playlist) - .WithMany(p => p.PlaylistSongs) - .HasForeignKey(ps => ps.PlaylistId); - - modelBuilder - .Entity() - .HasOne(ps => ps.Song) - .WithMany(s => s.PlaylistSongs) - .HasForeignKey(ps => ps.SongId); - } -} diff --git a/Console/Database/Setting.cs b/Console/Database/Setting.cs index 82982d7..b4e2bce 100644 --- a/Console/Database/Setting.cs +++ b/Console/Database/Setting.cs @@ -1,10 +1,10 @@ -using Microsoft.EntityFrameworkCore; +using Dapper.Contrib.Extensions; namespace Console.Database; -[PrimaryKey("Id")] internal class Setting { + [Key] public int Id { get; set; } public int Volume { get; set; } } diff --git a/Console/Extensions/MyDbContextExtensions.cs b/Console/Extensions/MyDbContextExtensions.cs deleted file mode 100644 index c8d46ef..0000000 --- a/Console/Extensions/MyDbContextExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Console.Database; -using Microsoft.EntityFrameworkCore; - -namespace Console.Extensions; - -internal static class MyDbContextExtensions -{ - public static async ValueTask MigrateAOTAsync(this MyDbContext context, string filename) - { - using Stream stream = File.OpenRead(filename); - using StreamReader reader = new(stream); - var sql = reader.ReadToEnd(); - await context.Database.ExecuteSqlRawAsync(sql); - } -} diff --git a/Console/Migrations/20240803220027_InitialCreate.Designer.cs b/Console/Migrations/20240803220027_InitialCreate.Designer.cs deleted file mode 100644 index 60fc329..0000000 --- a/Console/Migrations/20240803220027_InitialCreate.Designer.cs +++ /dev/null @@ -1,117 +0,0 @@ -// -using System; -using Console.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Console.Migrations -{ - [DbContext(typeof(MyDbContext))] - [Migration("20240803220027_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Property("PlaylistId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Description") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId"); - - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.Property("PlaylistId") - .HasColumnType("INTEGER"); - - b.Property("SongId") - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId", "SongId"); - - b.HasIndex("SongId"); - - b.ToTable("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ChannelTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Songs"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.HasOne("Console.Database.LocalPlaylist", "Playlist") - .WithMany("PlaylistSongs") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Console.Database.LocalSong", "Song") - .WithMany("PlaylistSongs") - .HasForeignKey("SongId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Playlist"); - - b.Navigation("Song"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Navigation("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Navigation("PlaylistSongs"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Console/Migrations/20240803220027_InitialCreate.cs b/Console/Migrations/20240803220027_InitialCreate.cs deleted file mode 100644 index c84e3de..0000000 --- a/Console/Migrations/20240803220027_InitialCreate.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Console.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Playlists", - columns: table => new - { - PlaylistId = table - .Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: false), - Description = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Playlists", x => x.PlaylistId); - } - ); - - migrationBuilder.CreateTable( - name: "Songs", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Url = table.Column(type: "TEXT", nullable: false), - Title = table.Column(type: "TEXT", nullable: false), - ChannelId = table.Column(type: "TEXT", nullable: false), - ChannelTitle = table.Column(type: "TEXT", nullable: false), - Duration = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Songs", x => x.Id); - } - ); - - migrationBuilder.CreateTable( - name: "PlaylistSongs", - columns: table => new - { - PlaylistId = table.Column(type: "INTEGER", nullable: false), - SongId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PlaylistSongs", x => new { x.PlaylistId, x.SongId }); - table.ForeignKey( - name: "FK_PlaylistSongs_Playlists_PlaylistId", - column: x => x.PlaylistId, - principalTable: "Playlists", - principalColumn: "PlaylistId", - onDelete: ReferentialAction.Cascade - ); - table.ForeignKey( - name: "FK_PlaylistSongs_Songs_SongId", - column: x => x.SongId, - principalTable: "Songs", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateIndex( - name: "IX_PlaylistSongs_SongId", - table: "PlaylistSongs", - column: "SongId" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "PlaylistSongs"); - - migrationBuilder.DropTable(name: "Playlists"); - - migrationBuilder.DropTable(name: "Songs"); - } - } -} diff --git a/Console/Migrations/20240803224221_RemoveDescription.Designer.cs b/Console/Migrations/20240803224221_RemoveDescription.Designer.cs deleted file mode 100644 index 545c09f..0000000 --- a/Console/Migrations/20240803224221_RemoveDescription.Designer.cs +++ /dev/null @@ -1,113 +0,0 @@ -// -using System; -using Console.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Console.Migrations -{ - [DbContext(typeof(MyDbContext))] - [Migration("20240803224221_RemoveDescription")] - partial class RemoveDescription - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Property("PlaylistId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId"); - - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.Property("PlaylistId") - .HasColumnType("INTEGER"); - - b.Property("SongId") - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId", "SongId"); - - b.HasIndex("SongId"); - - b.ToTable("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ChannelTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Songs"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.HasOne("Console.Database.LocalPlaylist", "Playlist") - .WithMany("PlaylistSongs") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Console.Database.LocalSong", "Song") - .WithMany("PlaylistSongs") - .HasForeignKey("SongId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Playlist"); - - b.Navigation("Song"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Navigation("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Navigation("PlaylistSongs"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Console/Migrations/20240803224221_RemoveDescription.cs b/Console/Migrations/20240803224221_RemoveDescription.cs deleted file mode 100644 index 6f85048..0000000 --- a/Console/Migrations/20240803224221_RemoveDescription.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Console.Migrations -{ - /// - public partial class RemoveDescription : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Description", - table: "Playlists"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Description", - table: "Playlists", - type: "TEXT", - nullable: false, - defaultValue: ""); - } - } -} diff --git a/Console/Migrations/20240804155033_Order.Designer.cs b/Console/Migrations/20240804155033_Order.Designer.cs deleted file mode 100644 index 1d93119..0000000 --- a/Console/Migrations/20240804155033_Order.Designer.cs +++ /dev/null @@ -1,116 +0,0 @@ -// -using System; -using Console.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Console.Migrations -{ - [DbContext(typeof(MyDbContext))] - [Migration("20240804155033_Order")] - partial class Order - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Property("PlaylistId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId"); - - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.Property("PlaylistId") - .HasColumnType("INTEGER"); - - b.Property("SongId") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.HasKey("PlaylistId", "SongId"); - - b.HasIndex("SongId"); - - b.ToTable("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ChannelTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Songs"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.HasOne("Console.Database.LocalPlaylist", "Playlist") - .WithMany("PlaylistSongs") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Console.Database.LocalSong", "Song") - .WithMany("PlaylistSongs") - .HasForeignKey("SongId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Playlist"); - - b.Navigation("Song"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Navigation("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Navigation("PlaylistSongs"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Console/Migrations/20240804155033_Order.cs b/Console/Migrations/20240804155033_Order.cs deleted file mode 100644 index a0fb11f..0000000 --- a/Console/Migrations/20240804155033_Order.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Console.Migrations -{ - /// - public partial class Order : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Order", - table: "PlaylistSongs", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Order", - table: "PlaylistSongs"); - } - } -} diff --git a/Console/Migrations/20240813103834_Settings.Designer.cs b/Console/Migrations/20240813103834_Settings.Designer.cs deleted file mode 100644 index 013b27a..0000000 --- a/Console/Migrations/20240813103834_Settings.Designer.cs +++ /dev/null @@ -1,130 +0,0 @@ -// -using System; -using Console.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Console.Migrations -{ - [DbContext(typeof(MyDbContext))] - [Migration("20240813103834_Settings")] - partial class Settings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Property("PlaylistId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId"); - - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.Property("PlaylistId") - .HasColumnType("INTEGER"); - - b.Property("SongId") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.HasKey("PlaylistId", "SongId"); - - b.HasIndex("SongId"); - - b.ToTable("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ChannelTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Songs"); - }); - - modelBuilder.Entity("Console.Database.Setting", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Volume") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.HasOne("Console.Database.LocalPlaylist", "Playlist") - .WithMany("PlaylistSongs") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Console.Database.LocalSong", "Song") - .WithMany("PlaylistSongs") - .HasForeignKey("SongId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Playlist"); - - b.Navigation("Song"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Navigation("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Navigation("PlaylistSongs"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Console/Migrations/20240813103834_Settings.cs b/Console/Migrations/20240813103834_Settings.cs deleted file mode 100644 index d19f064..0000000 --- a/Console/Migrations/20240813103834_Settings.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Console.Migrations -{ - /// - public partial class Settings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Settings", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Volume = table.Column(type: "REAL", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Settings", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Settings"); - } - } -} diff --git a/Console/Migrations/20240813104643_Volume_as_int.Designer.cs b/Console/Migrations/20240813104643_Volume_as_int.Designer.cs deleted file mode 100644 index 8c1757a..0000000 --- a/Console/Migrations/20240813104643_Volume_as_int.Designer.cs +++ /dev/null @@ -1,130 +0,0 @@ -// -using System; -using Console.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Console.Migrations -{ - [DbContext(typeof(MyDbContext))] - [Migration("20240813104643_Volume_as_int")] - partial class Volume_as_int - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Property("PlaylistId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId"); - - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.Property("PlaylistId") - .HasColumnType("INTEGER"); - - b.Property("SongId") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.HasKey("PlaylistId", "SongId"); - - b.HasIndex("SongId"); - - b.ToTable("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ChannelTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Songs"); - }); - - modelBuilder.Entity("Console.Database.Setting", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Volume") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.HasOne("Console.Database.LocalPlaylist", "Playlist") - .WithMany("PlaylistSongs") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Console.Database.LocalSong", "Song") - .WithMany("PlaylistSongs") - .HasForeignKey("SongId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Playlist"); - - b.Navigation("Song"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Navigation("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Navigation("PlaylistSongs"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Console/Migrations/20240813104643_Volume_as_int.cs b/Console/Migrations/20240813104643_Volume_as_int.cs deleted file mode 100644 index c635f98..0000000 --- a/Console/Migrations/20240813104643_Volume_as_int.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Console.Migrations -{ - /// - public partial class Volume_as_int : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Volume", - table: "Settings", - type: "INTEGER", - nullable: false, - oldClrType: typeof(float), - oldType: "REAL"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Volume", - table: "Settings", - type: "REAL", - nullable: false, - oldClrType: typeof(int), - oldType: "INTEGER"); - } - } -} diff --git a/Console/Migrations/MyDbContextModelSnapshot.cs b/Console/Migrations/MyDbContextModelSnapshot.cs deleted file mode 100644 index 6647d2a..0000000 --- a/Console/Migrations/MyDbContextModelSnapshot.cs +++ /dev/null @@ -1,127 +0,0 @@ -// -using System; -using Console.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Console.Migrations -{ - [DbContext(typeof(MyDbContext))] - partial class MyDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Property("PlaylistId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId"); - - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.Property("PlaylistId") - .HasColumnType("INTEGER"); - - b.Property("SongId") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.HasKey("PlaylistId", "SongId"); - - b.HasIndex("SongId"); - - b.ToTable("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ChannelTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Songs"); - }); - - modelBuilder.Entity("Console.Database.Setting", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Volume") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylistSong", b => - { - b.HasOne("Console.Database.LocalPlaylist", "Playlist") - .WithMany("PlaylistSongs") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Console.Database.LocalSong", "Song") - .WithMany("PlaylistSongs") - .HasForeignKey("SongId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Playlist"); - - b.Navigation("Song"); - }); - - modelBuilder.Entity("Console.Database.LocalPlaylist", b => - { - b.Navigation("PlaylistSongs"); - }); - - modelBuilder.Entity("Console.Database.LocalSong", b => - { - b.Navigation("PlaylistSongs"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Console/Program.cs b/Console/Program.cs index 71a249d..8253f13 100644 --- a/Console/Program.cs +++ b/Console/Program.cs @@ -1,25 +1,15 @@ using System.Diagnostics.CodeAnalysis; using CliFx; using Console.Commands; -using Console.Database; -using Console.Migrations; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Dapper; + +[module: DapperAot] namespace Console; public static class Program { [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MainCommand))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyDbContext))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(InitialCreate))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(RemoveDescription))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Order))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Settings))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Volume_as_int))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyDbContextModelSnapshot))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Migration))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ModelSnapshot))] public static async Task Main() => await new CliApplicationBuilder().AddCommand().Build().RunAsync(); } diff --git a/Console/Repositories/LocalPlaylistsRepository.cs b/Console/Repositories/LocalPlaylistsRepository.cs index 60b24de..2128d43 100644 --- a/Console/Repositories/LocalPlaylistsRepository.cs +++ b/Console/Repositories/LocalPlaylistsRepository.cs @@ -1,21 +1,74 @@ -using Console.Database; -using Microsoft.EntityFrameworkCore; +using System.Data; +using Console.Database; +using Dapper; using YoutubeExplode.Videos; namespace Console.Repositories; -internal class LocalPlaylistsRepository(MyDbContext db) : IDisposable +internal class LocalPlaylistsRepository(IDbConnection db) : IDisposable { public void Dispose() => db.Dispose(); - public async ValueTask> GetPlaylistsAsync() => - await db.Playlists.Include(i => i.PlaylistSongs).ToListAsync(); + public async ValueTask> GetPlaylistsAsync() + { + const string playlistsSql = "SELECT * FROM Playlists;"; + const string playlistSongsSql = + @" + SELECT * + FROM PlaylistSongs + WHERE PlaylistId IN (SELECT PlaylistId FROM Playlists);"; + + // Execute the queries + var playlists = (await db.QueryAsync(playlistsSql)).ToList(); + var playlistSongs = await db.QueryAsync(playlistSongsSql); + + // Map PlaylistSongs to Playlists + foreach (var playlist in playlists) + { + playlist.PlaylistSongs = playlistSongs + .Where(ps => ps.PlaylistId == playlist.PlaylistId) + .ToList(); + } + + return playlists; + } - public async ValueTask GetPlaylist(int id) => - await db - .Playlists.Include(i => i.PlaylistSongs) - .ThenInclude(i => i.Song) - .FirstOrDefaultAsync(i => i.PlaylistId == id); + public async ValueTask GetPlaylist(int id) + { + const string playlistSql = "SELECT * FROM Playlists WHERE PlaylistId = @Id;"; + const string playlistSongsSql = "SELECT * FROM PlaylistSongs WHERE PlaylistId = @Id;"; + const string songsSql = + @" + SELECT * + FROM Songs + WHERE Id IN (SELECT SongId FROM PlaylistSongs WHERE PlaylistId = @Id);"; + + // Execute the queries + var playlist = ( + await db.QueryAsync(playlistSql, new { Id = id }) + ).FirstOrDefault(); + var playlistSongs = await db.QueryAsync( + playlistSongsSql, + new { Id = id } + ); + + var songs = await db.QueryAsync(songsSql, new { Id = id }); + + if (playlist is not null) + { + playlist.PlaylistSongs = playlistSongs + .Where(ps => ps.PlaylistId == playlist.PlaylistId) + .ToList(); + + foreach (var item in playlist.PlaylistSongs) + { + item.Song = songs.FirstOrDefault(i => i.Id == item.SongId); + item.Playlist = playlist; + } + } + + return playlist; + } public async ValueTask SavePlaylist(string name, IEnumerable videos) { @@ -24,36 +77,49 @@ public async ValueTask SavePlaylist(string name, IEnumerable videos) { ChannelTitle = i.Author.ChannelTitle, ChannelId = i.Author.ChannelId, - Duration = i.Duration, + DurationMiliseconds = i.Duration.GetValueOrDefault().TotalMilliseconds, Title = i.Title, Url = i.Url, Id = i.Id, }) .ToList(); - foreach (var item in playlistVideos) - { - var alreadySong = await db.Songs.FirstOrDefaultAsync(i => i.Id == item.Id); + var songInsertSql = + @" + INSERT INTO Songs (Id, Title, ChannelTitle, ChannelId, DurationMiliseconds, Url) + VALUES (@Id, @Title, @ChannelTitle, @ChannelId, @DurationMiliseconds, @Url) + ON CONFLICT (Id) DO UPDATE + SET Title = excluded.Title, ChannelTitle = excluded.ChannelTitle, + ChannelId = excluded.ChannelId, DurationMiliseconds = excluded.DurationMiliseconds, Url = excluded.Url;"; - if (alreadySong is null) - { - await db.Songs.AddAsync(item); - } - else - { - alreadySong.Title = item.Title; - db.Songs.Update(alreadySong); - } + foreach (var song in playlistVideos) + { + await db.ExecuteAsync(songInsertSql, song); } - var localPlaylist = new LocalPlaylist - { - Name = name, - PlaylistSongs = playlistVideos - .Select((i, index) => new LocalPlaylistSong { SongId = i.Id, Order = index }) - .ToList(), - }; - await db.Playlists.AddAsync(localPlaylist); - await db.SaveChangesAsync(); + var playlistInsertSql = + @" + INSERT INTO Playlists (Name) + VALUES (@Name) + RETURNING PlaylistId;"; + + var playlistId = await db.QuerySingleAsync(playlistInsertSql, new { Name = name }); + + var playlistSongsInsertSql = + @" + INSERT INTO PlaylistSongs (PlaylistId, SongId, [Order]) + VALUES (@PlaylistId, @SongId, @Order);"; + + var playlistSongs = playlistVideos.Select( + (song, index) => + new + { + PlaylistId = playlistId, + SongId = song.Id, + Order = index + } + ); + + await db.ExecuteAsync(playlistSongsInsertSql, playlistSongs); } } diff --git a/Console/Repositories/SettingsRepository.cs b/Console/Repositories/SettingsRepository.cs index 4d33698..c420261 100644 --- a/Console/Repositories/SettingsRepository.cs +++ b/Console/Repositories/SettingsRepository.cs @@ -1,22 +1,25 @@ -using Console.Database; -using Microsoft.EntityFrameworkCore; +using System.Data; +using Console.Database; +using Dapper; +using Dapper.Contrib.Extensions; namespace Console.Repositories; -internal class SettingsRepository(MyDbContext db) : IDisposable, IAsyncDisposable +internal class SettingsRepository(IDbConnection db) : IDisposable { public void Dispose() => db.Dispose(); - public ValueTask DisposeAsync() => db.DisposeAsync(); - private Setting? _currentSettings; public async ValueTask InitializeAsync() { - if (!await db.Settings.AnyAsync()) + const string checkSettingsSql = "SELECT COUNT(*) FROM Settings;"; + const string insertDefaultSettingsSql = "INSERT INTO Settings (Volume) VALUES (50);"; + + var count = await db.ExecuteScalarAsync(checkSettingsSql); + if (count == 0) { - await db.Settings.AddAsync(new Setting { Volume = 50 }); - await db.SaveChangesAsync(); + await db.ExecuteAsync(insertDefaultSettingsSql); } } @@ -25,14 +28,15 @@ public async ValueTask SaveSettingsAsync( CancellationToken cancellationToken = default ) { - db.Settings.Update(settings); - await db.SaveChangesAsync(cancellationToken); + await db.UpdateAsync(settings); } public async ValueTask GetSettingsAsync(CancellationToken cancellation = default) { + const string selectSettingsSql = "SELECT * FROM Settings;"; + _currentSettings ??= - await db.Settings.FirstOrDefaultAsync(cancellationToken: cancellation) + await db.QuerySingleOrDefaultAsync(selectSettingsSql) ?? throw new Exception("Settings not initialized"); return _currentSettings; diff --git a/Console/Utils.cs b/Console/Utils.cs index cb86174..79993d5 100644 --- a/Console/Utils.cs +++ b/Console/Utils.cs @@ -1,5 +1,4 @@ -using CliFx.Infrastructure; -using PortAudioSharp; +using PortAudioSharp; using Terminal.Gui; namespace Console; @@ -8,12 +7,10 @@ public static class Utils { private static bool _isShowing = false; - public static async ValueTask ConfigurePlatformDependenciesAsync(IConsole console) + public static void ConfigurePlatformDependencies() { PortAudio.LoadNativeLibrary(); PortAudio.Initialize(); - await console.Output.WriteLineAsync("Ignore any warnings above this message"); - await console.Output.WriteLineAsync("[PortAudio] Initialized corretly"); } public static string? ShowInputDialog(string title, string prompt, ColorScheme colorScheme) diff --git a/Tests/PlayerTests.cs b/Tests/PlayerTests.cs index b8e7c2f..ff42c7b 100644 --- a/Tests/PlayerTests.cs +++ b/Tests/PlayerTests.cs @@ -23,7 +23,7 @@ public PlayerTests() var options = CreateInMemoryOptions(); var context = new MyDbContext(options); var settings = new SettingsRepository(context); - Utils.ConfigurePlatformDependenciesAsync(new FakeConsole()).GetAwaiter().GetResult(); + Utils.ConfigurePlatformDependencies(); settings.InitializeAsync().GetAwaiter().GetResult(); _player = new(new YoutubeClient(), settings) { Volume = 0 }; }