Compare commits

...

5 Commits

Author SHA1 Message Date
Kleidukos 6b5c5d0d3b Some changes and missing classes on last commit
- Program.cs updated to services and init database
2026-05-23 16:26:25 +02:00
Kleidukos adbc3a28d6 Services for scraping data from TCGdex 2026-05-23 16:24:51 +02:00
Kleidukos f71283d617 Created models forTCGdex Service to scrape data 2026-05-23 16:24:20 +02:00
Kleidukos ca78d6ace4 Setup DbContext 2026-05-23 16:23:18 +02:00
Kleidukos 400faa2fc3 Created entities for database 2026-05-23 16:22:52 +02:00
42 changed files with 1616 additions and 14 deletions
+171
View File
@@ -0,0 +1,171 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using OakArchive.Entities.CardMarket;
using OakArchive.Entities.Cards;
using OakArchive.Entities.Collection;
using OakArchive.Entities.Grading;
using OakArchive.Entities.Serie;
using OakArchive.Entities.Set;
using OakArchive.Entities.TCGdex;
using OakArchive.Entities.User;
namespace OakArchive.Database;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<Settings> Settings { get; set; } = null!;
public DbSet<Collection> Collections { get; set; } = null!;
public DbSet<CollectedCard> CollectedCards { get; set; } = null!;
public DbSet<Grading> Gradings { get; set; } = null!;
public DbSet<TcgDexCardInfo> TcgDexCardInfos { get; set; } = null!;
public DbSet<TcgDexSetInfo> TcgDexSetInfos { get; set; } = null!;
public DbSet<TcgDexSeriesInfo> TcgDexSeriesInfos { get; set; } = null!;
public DbSet<Serie> Series { get; set; } = null!;
public DbSet<Set> Sets { get; set; } = null!;
public DbSet<Legal> Legals { get; set; } = null!;
public DbSet<CardCount> CardCounts { get; set; } = null!;
public DbSet<Card> Cards { get; set; } = null!;
public DbSet<Attack> Attacks { get; set; } = null!;
public DbSet<Weakness> Weaknesses { get; set; } = null!;
public DbSet<CardMarketSetInfo> CardMarketSetInfos { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 1. Basis-Mapping für ASP.NET Core Identity (Pflicht!)
base.OnModelCreating(modelBuilder);
#region User & Settings
modelBuilder.Entity<ApplicationUser>(entity =>
{
// Konfiguration für das private Feld 'DefaultCollection' als Backing Field
entity.Property<Guid>("DefaultCollectionId");
entity.HasOne<Collection>()
.WithMany()
.HasForeignKey("DefaultCollectionId")
.OnDelete(DeleteBehavior.Restrict);
// n:m Beziehung zwischen Usern und Collections (Members)
entity.HasMany(u => u.Collections)
.WithMany(c => c.Members)
.UsingEntity(j => j.ToTable("UserCollections"));
});
modelBuilder.Entity<Settings>()
.HasOne(s => s.User)
.WithOne(u => u.Settings)
.HasForeignKey<Settings>(s => s.UserId)
.OnDelete(DeleteBehavior.Cascade);
#endregion
#region 1:1 TCGdex & CardMarket Mapping
// EF Core mitgeteilt, auf welcher Seite der FK physisch liegt
modelBuilder.Entity<Card>()
.HasOne(c => c.TcgDexCardInfo)
.WithOne(t => t.Card)
.HasForeignKey<TcgDexCardInfo>(t => t.CardId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Set>()
.HasOne(s => s.SetInfo)
.WithOne(t => t.Set)
.HasForeignKey<TcgDexSetInfo>(t => t.SetIdRef)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Serie>()
.HasOne(s => s.TcgDexSeriesInfo)
.WithOne(t => t.Series)
.HasForeignKey<TcgDexSeriesInfo>(t => t.SeriesIdRef)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Set>()
.HasOne(s => s.CardCount)
.WithOne(c => c.Set)
.HasForeignKey<CardCount>(c => c.SetId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Set>()
.HasOne(s => s.CardMarketSetInfo)
.WithOne(cms => cms.Set)
.HasForeignKey<CardMarketSetInfo>(cms => cms.SetId)
.OnDelete(DeleteBehavior.Cascade);
// Auflösung der geteilten Legal-Tabelle
modelBuilder.Entity<Legal>(entity =>
{
entity.HasOne(l => l.Set)
.WithOne(s => s.Legal)
.HasForeignKey<Legal>(l => l.SetId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(l => l.Card)
.WithOne(c => c.Legal)
.HasForeignKey<Legal>(l => l.CardId)
.OnDelete(DeleteBehavior.Cascade);
});
#endregion
#region Postgres JSONB Columns
modelBuilder.Entity<Card>(entity =>
{
entity.HasOne(c => c.Set)
.WithMany(s => s.Cards)
.HasForeignKey(c => c.SetId)
.OnDelete(DeleteBehavior.Restrict);
// Native .NET Listen als JSONB in Postgres speichern
entity.Property(c => c.Variants).HasColumnType("jsonb");
entity.Property(c => c.Types).HasColumnType("jsonb");
});
modelBuilder.Entity<Attack>(entity =>
{
entity.HasOne(a => a.Card)
.WithMany(c => c.Attacks)
.HasForeignKey(a => a.CardId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(a => a.Cost).HasColumnType("jsonb");
});
#endregion
#region CollectedCard & Grading
modelBuilder.Entity<CollectedCard>(entity =>
{
entity.HasOne(cc => cc.Collection)
.WithMany(c => c.Cards)
.HasForeignKey(cc => cc.CollectionId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(cc => cc.Card)
.WithMany()
.HasForeignKey(cc => cc.CardId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(cc => cc.ScannedBy)
.WithMany()
.HasForeignKey(cc => cc.ScannedById)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Grading>()
.HasOne(g => g.CollectedCard)
.WithOne(cc => cc.Grading)
.HasForeignKey<Grading>(g => g.CollectedCardId)
.OnDelete(DeleteBehavior.Cascade);
#endregion
}
}
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.Cards;
namespace OakArchive.Entities.CardMarket;
public class CardMarketSetInfo
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
public string ExpansionId { get; set; } = string.Empty;
[ForeignKey(nameof(Set))] public Guid SetId { get; set; }
public virtual Set.Set Set { get; set; } = null!;
}
+21
View File
@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
namespace OakArchive.Entities.Cards;
public class Attack
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
public List<string> Cost { get; set; } = [];
public string Name { get; set; } = string.Empty;
public int? Damage { get; set; }
public string Effect { get; set; } = string.Empty;
[ForeignKey(nameof(Card))] public Guid CardId { get; set; }
public virtual Card Card { get; set; } = null!;
}
+57
View File
@@ -0,0 +1,57 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.CardMarket;
using OakArchive.Entities.Enums;
using OakArchive.Entities.Set;
using OakArchive.Entities.TCGdex;
namespace OakArchive.Entities.Cards;
public class Card
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
[Required] public string Name { get; set; } = string.Empty;
public string Image { get; set; } = string.Empty;
public int CardNumber { get; set; } = 0;
public ulong PHash { get; set; } = 0;
public int? Hp { get; set; }
public int? Retreat { get; set; }
public string? TrainerType { get; set; }
public string? EnergyType { get; set; }
public string? Illustrator { get; set; }
public string? EvolveFrom { get; set; }
public string? Description { get; set; }
public string? RegulationMark { get; set; }
public Rarity? Rarity { get; set; }
public Category? Category { get; set; }
[Column(TypeName = "jsonb")]
public List<Variant>? Variants { get; set; }
[Column(TypeName = "jsonb")] public List<string>? Types { get; set; }
[ForeignKey(nameof(Set))] public Guid SetId { get; set; }
public virtual Set.Set Set { get; set; } = null!;
public virtual TcgDexCardInfo TcgDexCardInfo { get; set; } = null!;
public virtual Legal? Legal { get; set; }
public virtual ICollection<Attack> Attacks { get; set; } = new List<Attack>();
public virtual ICollection<Weakness> Weaknesses { get; set; } = new List<Weakness>();
}
@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.Enums;
using OakArchive.Entities.User;
namespace OakArchive.Entities.Cards;
public class CollectedCard
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
public Variant Variant { get; set; } = Variant.Normal;
public Condition Condition { get; set; } = Condition.Mint;
public int Count { get; set; } = 1;
public Language Language { get; set; } = Language.En;
public DateTime ScannedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(ScannedBy))] public Guid ScannedById { get; set; }
public ApplicationUser ScannedBy { get; set; } = null!;
[ForeignKey(nameof(Collection))] public Guid CollectionId { get; set; }
public virtual Collection.Collection Collection { get; set; } = null!;
[ForeignKey(nameof(Card))] public Guid CardId { get; set; }
public virtual Card Card { get; set; } = null!;
public virtual Grading.Grading? Grading { get; set; }
}
+16
View File
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace OakArchive.Entities.Cards;
public class Weakness
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
public string Type { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
[ForeignKey(nameof(Card))] public Guid CardId { get; set; }
public virtual Card Card { get; set; } = null!;
}
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using OakArchive.Entities.Cards;
using OakArchive.Entities.User;
namespace OakArchive.Entities.Collection;
public class Collection
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid CreatedBy { get; set; } = Guid.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string InviteCode { get; set; } = string.Empty;
public bool IsPublic { get; set; } = false;
public virtual ICollection<ApplicationUser> Members { get; set; } = [];
public virtual ICollection<CollectedCard> Cards { get; set; } = [];
}
+16
View File
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace OakArchive.Entities.Enums;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Category
{
[JsonPropertyName("Energy")]
Energy,
[JsonPropertyName("Pokemon")]
Pokemon,
[JsonPropertyName("Trainer")]
Trainer
}
+12
View File
@@ -0,0 +1,12 @@
namespace OakArchive.Entities.Enums;
public enum Condition
{
Mint = 0,
NearMint = 1,
Excellent = 2,
Good = 3,
LightPlayed = 4,
Played = 5,
Poor = 6
}
@@ -0,0 +1,14 @@
namespace OakArchive.Entities.Enums;
public enum GradingCompany
{
PSA,
BGS,
CGC,
APGrading,
EGS,
AOG,
PCA,
GSG,
Other
}
+25
View File
@@ -0,0 +1,25 @@
using System.ComponentModel;
namespace OakArchive.Entities.Enums;
public enum Language
{
En,
Fr,
Es,
Es_Mx,
It,
Pt,
Pt_Br,
Pt_Pt,
De,
Nl,
Pl,
Ru,
Ja,
Ko,
Zh_Tw,
Id,
Th,
Zh_Cn
}
+124
View File
@@ -0,0 +1,124 @@
using System.Text.Json.Serialization;
namespace OakArchive.Entities.Enums;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Rarity
{
[JsonPropertyName("ACE SPEC Rare")]
AceSpecRare,
[JsonPropertyName("Amazing Rare")]
AmazingRare,
[JsonPropertyName("Black White Rare")]
BlackWhiteRare,
[JsonPropertyName("Classic Collection")]
ClassicCollection,
[JsonPropertyName("Common")]
Common,
[JsonPropertyName("Crown")]
Crown,
[JsonPropertyName("Double rare")]
DoubleRare,
[JsonPropertyName("Four Diamond")]
FourDiamond,
[JsonPropertyName("Full Art Trainer")]
FullArtTrainer,
[JsonPropertyName("Holo Rare")]
HoloRare,
[JsonPropertyName("Holo Rare V")]
HoloRareV,
[JsonPropertyName("Holo Rare VMAX")]
HoloRareVmax,
[JsonPropertyName("Holo Rare VSTAR")]
HoloRareVstar,
[JsonPropertyName("Hyper rare")]
HyperRare,
[JsonPropertyName("Illustration rare")]
IllustrationRare,
[JsonPropertyName("LEGEND")]
Legend,
[JsonPropertyName("Mega Hyper Rare")]
MegaHyperRare,
[JsonPropertyName("None")]
None,
[JsonPropertyName("One Diamond")]
OneDiamond,
[JsonPropertyName("One Shiny")]
OneShiny,
[JsonPropertyName("One Star")]
OneStar,
[JsonPropertyName("Radiant Rare")]
RadiantRare,
[JsonPropertyName("Rare")]
Rare,
[JsonPropertyName("Rare Holo")]
RareHolo,
[JsonPropertyName("Rare Holo LV.X")]
RareHoloLvX,
[JsonPropertyName("Rare PRIME")]
RarePrime,
[JsonPropertyName("Secret Rare")]
SecretRare,
[JsonPropertyName("Shiny Ultra Rare")]
ShinyUltraRare,
[JsonPropertyName("Shiny rare")]
ShinyRare,
[JsonPropertyName("Shiny rare V")]
ShinyRareV,
[JsonPropertyName("Shiny rare VMAX")]
ShinyRareVmax,
[JsonPropertyName("Special illustration rare")]
SpecialIllustrationRare,
[JsonPropertyName("Three Diamond")]
ThreeDiamond,
[JsonPropertyName("Three Star")]
ThreeStar,
[JsonPropertyName("Two Diamond")]
TwoDiamond,
[JsonPropertyName("Two Shiny")]
TwoShiny,
[JsonPropertyName("Two Star")]
TwoStar,
[JsonPropertyName("Ultra Rare")]
UltraRare,
[JsonPropertyName("Uncommon")]
Uncommon
}
+37
View File
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
namespace OakArchive.Entities.Enums;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Stage
{
[JsonPropertyName("BREAK")]
Break,
[JsonPropertyName("Basic")]
Basic,
[JsonPropertyName("LEVEL-UP")]
LevelUp,
[JsonPropertyName("MEGA")]
Mega,
[JsonPropertyName("RESTORED")]
Restored,
[JsonPropertyName("Stage1")]
Stage1,
[JsonPropertyName("Stage2")]
Stage2,
[JsonPropertyName("V-UNION")]
VUnion,
[JsonPropertyName("VMAX")]
VMax,
[JsonPropertyName("VSTAR")]
VStar
}
+22
View File
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
namespace OakArchive.Entities.Enums;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Variant
{
[JsonPropertyName("firstEdition")]
FirstEdition,
[JsonPropertyName("holo")]
Holo,
[JsonPropertyName("normal")]
Normal,
[JsonPropertyName("reverse")]
Reverse,
[JsonPropertyName("wPromo")]
WPromo
}
+28
View File
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.Cards;
using OakArchive.Entities.Enums;
namespace OakArchive.Entities.Grading;
public class Grading
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
[Required] public GradingCompany Company { get; set; } = GradingCompany.PSA;
[Required] public string CertificateNumber { get; set; } = string.Empty;
public double Grade { get; set; }
public double? Centering { get; set; }
public double? Corners { get; set; }
public double? Edges { get; set; }
public double? Surface { get; set; }
public string? GraderNotes { get; set; }
public DateTime GradedAt { get; set; }
[ForeignKey(nameof(CollectedCard))] public Guid CollectedCardId { get; set; }
public virtual CollectedCard CollectedCard { get; set; } = null!;
}
+24
View File
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using OakArchive.Entities.Enums;
using OakArchive.Entities.TCGdex;
namespace OakArchive.Entities.Serie;
public class Serie
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public string Name { get; set; } = string.Empty;
public string? Logo { get; set; }
public Language? Language { get; set; }
//public DateTime? ReleaseDate { get; set; }
public virtual ICollection<Set.Set> Sets { get; set; } = new List<Set.Set>();
public virtual TcgDexSeriesInfo TcgDexSeriesInfo { get; set; } = null!;
}
+26
View File
@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace OakArchive.Entities.Set;
public class CardCount
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
public int Total { get; set; } = 0;
public int Official { get; set; } = 0;
public int Normal { get; set; } = 0;
public int Reverse { get; set; } = 0;
public int Holo { get; set; } = 0;
public int FirstEd { get; set; } = 0;
[ForeignKey(nameof(Set))]
public Guid SetId { get; set; }
public virtual Set Set { get; set; } = null!;
}
+22
View File
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.Cards;
namespace OakArchive.Entities.Set;
public class Legal
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
public bool Expanded { get; set; } = false;
public bool Standard { get; set; } = false;
[ForeignKey(nameof(Set))]
public Guid? SetId { get; set; }
public virtual Set? Set { get; set; }
[ForeignKey(nameof(Card))]
public Guid? CardId { get; set; }
public virtual Card? Card { get; set; }
}
+36
View File
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.CardMarket;
using OakArchive.Entities.Cards;
using OakArchive.Entities.Enums;
using OakArchive.Entities.TCGdex;
namespace OakArchive.Entities.Set;
public class Set
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
[Required] public string Name { get; set; } = string.Empty;
public string? Symbol { get; set; }
public string? Logo { get; set; }
public Language Language { get; set; } = Language.En;
public DateTime? ReleaseDate { get; set; }
public ICollection<Card> Cards { get; set; } = new List<Card>();
public virtual TcgDexSetInfo SetInfo { get; set; } = null!;
public virtual CardMarketSetInfo CardMarketSetInfo { get; set; } = null!;
[ForeignKey(nameof(Serie))] public Guid SerieId { get; set; }
public virtual Serie.Serie Serie { get; set; } = null!;
public virtual CardCount CardCount { get; set; } = null!;
public virtual Legal? Legal { get; set; }
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OakArchive.Entities.Cards;
namespace OakArchive.Entities.TCGdex;
public class TcgDexCardInfo
{
[Key] public Guid Id { get; set; } = Guid.NewGuid();
[Required] public string FullId { get; set; } = string.Empty;
[Required] public int LocalId { get; set; } = 0;
[ForeignKey(nameof(Card))] public Guid CardId { get; set; }
public virtual Card Card { get; set; } = null!;
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace OakArchive.Entities.TCGdex;
public class TcgDexSeriesInfo
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public string SeriesId { get; set; } = string.Empty;
[ForeignKey(nameof(Set))]
public Guid SeriesIdRef { get; set; }
public virtual Serie.Serie Series { get; set; } = null!;
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace OakArchive.Entities.TCGdex;
public class TcgDexSetInfo
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public string SetId { get; set; } = string.Empty;
[ForeignKey(nameof(Set))]
public Guid SetIdRef { get; set; }
public virtual Set.Set Set { get; set; } = null!;
}
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Identity;
namespace OakArchive.Entities.User;
public class ApplicationUser : IdentityUser<Guid>
{
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public virtual Settings Settings { get; set; } = new();
private Collection.Collection DefaultCollection { get; set; } = new();
public virtual ICollection<Collection.Collection> Collections { get; set; } = [];
}
+16
View File
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace OakArchive.Entities.User;
public class Settings
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
public bool AddCardsToDefaultList { get; set; } = true;
[ForeignKey(nameof(User))]
public Guid UserId { get; set; }
public virtual ApplicationUser User { get; set; } = null!;
}
+18
View File
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class Attack
{
[JsonPropertyName("cost")]
public List<string>? Cost { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("effect")]
public string? Effect { get; set; }
[JsonPropertyName("damage")]
public int? Damage { get; set; }
}
+64
View File
@@ -0,0 +1,64 @@
using System.Text.Json.Serialization;
using OakArchive.Entities.Set;
namespace OakArchive.Models.TcgDex;
public class Card
{
[JsonPropertyName("category")]
public string? Category { get; set; }
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("illustrator")]
public string? Illustrator { get; set; }
[JsonPropertyName("image")]
public string? Image { get; set; }
[JsonPropertyName("localId")]
public int? LocalId { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("rarity")]
public string? Rarity { get; set; }
[JsonPropertyName("set")]
public Entities.Set.Set? Set { get; set; }
[JsonPropertyName("variants")]
public Variants? Variants { get; set; }
[JsonPropertyName("hp")]
public int? Hp { get; set; }
[JsonPropertyName("types")]
public List<string>? Types { get; set; }
[JsonPropertyName("evolveFrom")]
public string? EvolveFrom { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("stage")]
public string? Stage { get; set; }
[JsonPropertyName("attacks")]
public List<Attack>? Attacks { get; set; }
[JsonPropertyName("weaknesses")]
public List<Weakness>? Weaknesses { get; set; }
[JsonPropertyName("retreat")]
public int? Retreat { get; set; }
[JsonPropertyName("regulationMark")]
public string? RegulationMark { get; set; }
[JsonPropertyName("legal")]
public Legal? Legal { get; set; }
}
+24
View File
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class CardCount
{
[JsonPropertyName("firstEd")]
public int? FirstEd { get; set; }
[JsonPropertyName("holo")]
public int? Holo { get; set; }
[JsonPropertyName("normal")]
public int? Normal { get; set; }
[JsonPropertyName("official")]
public int? Official { get; set; }
[JsonPropertyName("reverse")]
public int? Reverse { get; set; }
[JsonPropertyName("total")]
public int? Total { get; set; }
}
+12
View File
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class Legal
{
[JsonPropertyName("standard")]
public bool? Standard { get; set; }
[JsonPropertyName("expanded")]
public bool? Expanded { get; set; }
}
+18
View File
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class Serie
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("logo")]
public string? Logo { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("sets")]
public List<SetBrief>? Sets { get; set; }
}
+15
View File
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class SerieBrief
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("logo")]
public string? Logo { get; set; }
}
+33
View File
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class Set
{
[JsonPropertyName("cardCount")]
public CardCount CardCount { get; set; }
[JsonPropertyName("cards")]
public List<Card> Cards { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("legal")]
public Legal Legal { get; set; }
[JsonPropertyName("logo")]
public string Logo { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("releaseDate")]
public DateTime ReleaseDate { get; set; }
[JsonPropertyName("serie")]
public Serie Serie { get; set; }
[JsonPropertyName("symbol")]
public string Symbol { get; set; }
}
+21
View File
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class SetBrief
{
[JsonPropertyName("cardCount")]
public CardCount? CardCount { get; set; }
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("logo")]
public string? Logo { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("symbol")]
public string? Symbol { get; set; }
}
+21
View File
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class Variants
{
[JsonPropertyName("firstEdition")]
public bool? FirstEdition { get; set; }
[JsonPropertyName("holo")]
public bool? Holo { get; set; }
[JsonPropertyName("normal")]
public bool? Normal { get; set; }
[JsonPropertyName("reverse")]
public bool? Reverse { get; set; }
[JsonPropertyName("wPromo")]
public bool? WPromo { get; set; }
}
+12
View File
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace OakArchive.Models.TcgDex;
public class Weakness
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("value")]
public string? Value { get; set; }
}
+2 -7
View File
@@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
@@ -28,13 +29,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
<Folder Include="Entities\CardMarket\" />
<Folder Include="Entities\Cards\" />
<Folder Include="Entities\Collection\" />
<Folder Include="Entities\TCGdex\" />
<Folder Include="Entities\User\" />
<Folder Include="Models\" />
<Folder Include="Controllers\V1\" />
</ItemGroup>
</Project>
+49 -3
View File
@@ -1,21 +1,67 @@
using Microsoft.EntityFrameworkCore;
using OakArchive.Database;
using OakArchive.Services;
namespace OakArchive;
public class Program
{
public static void Main(string[] args)
public static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration["DefaultConnection"]));
builder.Services.AddMemoryCache(); // Registriert das native IMemoryCache von Microsoft
builder.Services.AddSingleton<IMemoryCacheService, MemoryCacheService>();
builder.Services.AddHttpClient<ITcgDexService, TcgDexService>(client =>
{
client.BaseAddress = new Uri("https://api.tcgdex.net/v2/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
builder.Services.AddScoped<ITcgDexDataUpdater, TcgDexDataUpdater>();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.Lifetime.ApplicationStarted.Register(async () =>
{
// Da ApplicationStarted synchron registriert, machen wir hier einen neuen asynchronen Scope auf
using var scope = app.Services.CreateScope();
try
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
Console.WriteLine("[INFO] Prüfe auf ausstehende Datenbank-Migrationen...");
// Führt alle ausstehenden Migrationen auf der PostgreSQL-Datenbank aus.
// Falls die Datenbank noch gar nicht existiert, wird sie hier automatisch erstellt!
await context.Database.MigrateAsync();
var updater = scope.ServiceProvider.GetRequiredService<ITcgDexDataUpdater>();
Console.WriteLine("[INFO] === Starte TCGdex Test-Sync im Hintergrund ===");
var result = await updater.SyncBaseDataFromTcgDex();
Console.WriteLine($"[INFO] === Test-Sync beendet! Neue Daten geladen: {result} ===");
}
catch (Exception ex)
{
Console.WriteLine($"[FEHLER] !!! Fehler beim Test-Sync: {ex.Message} !!!");
Console.WriteLine(ex.StackTrace);
}
});
app.UseAuthorization();
app.UseStaticFiles();
app.MapControllers();
+50
View File
@@ -0,0 +1,50 @@
using Microsoft.Extensions.Caching.Memory;
namespace OakArchive.Services;
public interface IMemoryCacheService
{
Task<T?> GetOrCreateAsync<T>(string cacheKey, Func<Task<T?>> createItem, TimeSpan? expiration = null)
where T : class;
void Remove(string cacheKey);
}
public class MemoryCacheService : IMemoryCacheService
{
private readonly IMemoryCache _memoryCache;
private readonly TimeSpan _defaultExpiration = TimeSpan.FromHours(1);
public MemoryCacheService(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public async Task<T?> GetOrCreateAsync<T>(string cacheKey, Func<Task<T?>> createItem, TimeSpan? expiration = null)
where T : class
{
if (_memoryCache.TryGetValue(cacheKey, out T? cachedItem))
{
return cachedItem;
}
var item = await createItem();
if (item == null)
{
return null;
}
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expiration ?? _defaultExpiration);
_memoryCache.Set(cacheKey, item, cacheOptions);
return item!;
}
public void Remove(string cacheKey)
{
_memoryCache.Remove(cacheKey);
}
}
+261
View File
@@ -0,0 +1,261 @@
using Microsoft.EntityFrameworkCore;
using OakArchive.Database;
using OakArchive.Entities.CardMarket;
using OakArchive.Entities.Cards;
using OakArchive.Entities.Enums;
using OakArchive.Entities.TCGdex;
using OakArchive.Util;
using OakArchive.Util.Extensions;
using Card = OakArchive.Entities.Cards.Card;
using CardCount = OakArchive.Entities.Set.CardCount;
using Legal = OakArchive.Entities.Set.Legal;
using Set = OakArchive.Entities.Set.Set;
namespace OakArchive.Services;
public interface ITcgDexDataUpdater
{
public Task<bool> SyncBaseDataFromTcgDex();
public Task<Card>
UpdateCard(string set, int id, string language);
}
public class TcgDexDataUpdater : ITcgDexDataUpdater
{
private readonly ITcgDexService _tcgDexService;
private readonly ApplicationDbContext _dbContext;
private readonly List<Language> _languages = Enum.GetValues<Language>().ToList();
public TcgDexDataUpdater(ITcgDexService tcgDexService, ApplicationDbContext dbContext)
{
_tcgDexService = tcgDexService;
_dbContext = dbContext;
}
public async Task<bool> SyncBaseDataFromTcgDex()
{
var hasChanges = false;
int i = 0;
foreach (var language in _languages)
{
var apiSeriesBriefs = await _tcgDexService.GetSeries(language.ToApiCode());
await Task.Delay(500);
foreach (var serieBrief in apiSeriesBriefs)
{
if (i == 1) return true;
i += 1;
if (string.IsNullOrEmpty(serieBrief.Id)) continue;
var dbSerie = await EnsureSerieExistsAsync(serieBrief, language);
var fullApiSerie = await _tcgDexService.GetSetsOfSeries(serieBrief.Id, language.ToApiCode());
if (fullApiSerie?.Sets == null) continue;
await Task.Delay(300);
foreach (var setBrief in fullApiSerie.Sets)
{
if (string.IsNullOrEmpty(setBrief.Id)) continue;
var isSetInDb = dbSerie.Sets.Any(s => s.SetInfo?.SetId == setBrief.Id);
if (isSetInDb) continue;
var fullApiSet = await _tcgDexService.GetSet(setBrief.Id, language.ToApiCode());
if (fullApiSet == null) continue;
var dbSet = await BuildSetEntityWithCardsAsync(setBrief, fullApiSet, language);
dbSerie.Sets.Add(dbSet);
await Task.Delay(500);
}
if (await _dbContext.SaveChangesAsync() > 0)
{
hasChanges = true;
}
}
}
return hasChanges;
}
public async Task<Card> UpdateCard(string set, int id, string language)
{
var tcgDexCardId = $"{set.ToLower()}-{id}";
if (!Enum.TryParse<Language>(language, true, out var langEnum))
{
langEnum = Language.En;
}
var dbCard = await _dbContext.Cards
.Include(c => c.TcgDexCardInfo)
.Include(c => c.Attacks)
.Include(c => c.Weaknesses)
.FirstOrDefaultAsync(c => c.TcgDexCardInfo.FullId == tcgDexCardId && c.Set.Language == langEnum);
if (dbCard == null)
{
throw new KeyNotFoundException(
$"Die Basis-Karte mit der TCGdex-ID '{tcgDexCardId}' wurde für die Sprache '{language}' nicht in der Datenbank gefunden. Führe zuerst den Basis-Sync aus.");
}
var apiCard = await _tcgDexService.GetCard(set, id, language);
if (apiCard == null)
{
throw new HttpRequestException(
$"Die Karte '{tcgDexCardId}' konnte von der TCGdex-API nicht geladen werden.");
}
dbCard.Name = apiCard.Name ?? dbCard.Name;
dbCard.Hp = apiCard.Hp;
dbCard.Retreat = apiCard.Retreat;
dbCard.Illustrator = apiCard.Illustrator;
dbCard.EvolveFrom = apiCard.EvolveFrom;
dbCard.Description = apiCard.Description;
dbCard.RegulationMark = apiCard.RegulationMark;
dbCard.Types = apiCard.Types;
if (!string.IsNullOrEmpty(apiCard.Rarity) &&
Enum.TryParse<Rarity>(apiCard.Rarity.Replace(" ", ""), true, out var parsedRarity))
{
dbCard.Rarity = parsedRarity;
}
if (!string.IsNullOrEmpty(apiCard.Category) &&
Enum.TryParse<Category>(apiCard.Category, true, out var parsedCategory))
{
dbCard.Category = parsedCategory;
}
dbCard.Variants = MapApiVariantsToEnumList(apiCard.Variants);
dbCard.Attacks.Clear();
if (apiCard.Attacks != null)
{
foreach (var apiAttack in apiCard.Attacks)
{
dbCard.Attacks.Add(new Attack
{
Name = apiAttack.Name ?? "Unknown",
Damage = apiAttack.Damage,
Effect = apiAttack.Effect ?? string.Empty,
Cost = apiAttack.Cost ?? []
});
}
}
dbCard.Weaknesses.Clear();
if (apiCard.Weaknesses != null)
{
foreach (var apiWeakness in apiCard.Weaknesses)
{
dbCard.Weaknesses.Add(new Weakness
{
Type = apiWeakness.Type ?? "Unknown",
Value = apiWeakness.Value ?? string.Empty
});
}
}
await _dbContext.SaveChangesAsync();
return dbCard;
}
private List<Variant> MapApiVariantsToEnumList(Models.TcgDex.Variants? apiVariants)
{
var list = new List<Variant>();
if (apiVariants == null) return list;
if (apiVariants.FirstEdition == true) list.Add(Variant.FirstEdition);
if (apiVariants.Holo == true) list.Add(Variant.Holo);
if (apiVariants.Normal == true) list.Add(Variant.Normal);
if (apiVariants.Reverse == true) list.Add(Variant.Reverse);
if (apiVariants.WPromo == true) list.Add(Variant.WPromo);
return list;
}
private async Task<Entities.Serie.Serie> EnsureSerieExistsAsync(Models.TcgDex.SerieBrief brief, Language language)
{
var dbSerie = await _dbContext.Series
.Include(s => s.Sets)
.ThenInclude(s => s.SetInfo)
.FirstOrDefaultAsync(s => s.TcgDexSeriesInfo.SeriesId == brief.Id && s.Language == language);
if (dbSerie != null) return dbSerie;
dbSerie = new Entities.Serie.Serie
{
Language = language,
Name = brief.Name ?? "Unknown",
Logo = brief.Logo,
TcgDexSeriesInfo = new TcgDexSeriesInfo { SeriesId = brief.Id! }
};
_dbContext.Series.Add(dbSerie);
return dbSerie;
}
private async Task<Set> BuildSetEntityWithCardsAsync(Models.TcgDex.SetBrief setBrief, Models.TcgDex.Set fullApiSet,
Language language)
{
DateTime? utcReleaseDate = fullApiSet.ReleaseDate != null
? DateTime.SpecifyKind(fullApiSet.ReleaseDate, DateTimeKind.Utc)
: null;
var dbSet = new Set
{
Language = language,
Name = setBrief.Name ?? "Unknown",
Logo = setBrief.Logo,
ReleaseDate = utcReleaseDate,
Symbol = setBrief.Symbol,
SetInfo = new TcgDexSetInfo { SetId = fullApiSet.Id },
CardMarketSetInfo = new CardMarketSetInfo { ExpansionId = "0" }, // TODO: Späterer Price-Scraper
Legal = new Legal
{
Expanded = fullApiSet.Legal?.Expanded ?? false,
Standard = fullApiSet.Legal?.Standard ?? false
},
CardCount = new CardCount
{
FirstEd = fullApiSet.CardCount?.FirstEd ?? 0,
Holo = fullApiSet.CardCount?.Holo ?? 0,
Normal = fullApiSet.CardCount?.Normal ?? 0,
Official = fullApiSet.CardCount?.Official ?? 0,
Reverse = fullApiSet.CardCount?.Reverse ?? 0,
Total = fullApiSet.CardCount?.Total ?? 0
}
};
if (fullApiSet.Cards is not { Count: > 0 }) return dbSet;
var cardTasks = fullApiSet.Cards.Select(async c =>
{
var localPath = await _tcgDexService.DownloadCardImage(c, language.ToApiCode());
return new Card
{
Name = c.Name ?? "Unknown",
Image = localPath == null ? c.Image : localPath.Item1,
CardNumber = c.LocalId ?? 0,
PHash = localPath == null ? 0 : PerceptualHashHelper.GenerateHash(localPath.Item2),
TcgDexCardInfo = new TcgDexCardInfo
{
FullId = c.Id ?? string.Empty,
LocalId = c.LocalId ?? 0
}
};
});
dbSet.Cards = (await Task.WhenAll(cardTasks)).ToList();
return dbSet;
}
}
+113
View File
@@ -0,0 +1,113 @@
using OakArchive.Models.TcgDex;
namespace OakArchive.Services;
public interface ITcgDexService
{
public Task<List<SerieBrief>> GetSeries(string language);
public Task<Serie?> GetSetsOfSeries(string series, string language);
public Task<Set?> GetSet(string set, string language);
public Task<Card?> GetCard(string set, int id, string language);
public Task<Tuple<string, byte[]>?> DownloadCardImage(Card card, string language);
}
public class TcgDexService : ITcgDexService
{
private readonly HttpClient _httpClient;
private readonly IMemoryCacheService _cacheService;
private readonly string _baseFolder = Path.Combine(Directory.GetCurrentDirectory(), "Cards");
private readonly SemaphoreSlim _downloadSemaphore = new SemaphoreSlim(3, 3);
public TcgDexService(HttpClient httpClient, IMemoryCacheService cacheService)
{
_httpClient = httpClient;
_cacheService = cacheService;
}
public async Task<List<SerieBrief>> GetSeries(string language)
{
var cacheKey = $"tcgdex_series_{language.ToLower()}";
async Task<List<SerieBrief>> FetchFromApi()
{
var res = await _httpClient.GetFromJsonAsync<List<SerieBrief>>($"{language}/series");
return res ?? [];
}
return await _cacheService.GetOrCreateAsync(cacheKey, FetchFromApi, TimeSpan.FromDays(7));
}
public async Task<Serie?> GetSetsOfSeries(string series, string language)
{
var cacheKey = $"tcgdex_serie_{series.ToLower()}_{language.ToLower()}";
async Task<Serie?> FetchFromApi()
{
var res = await _httpClient.GetFromJsonAsync<Serie>($"{language}/series/{series}");
return res;
}
return await _cacheService.GetOrCreateAsync(cacheKey, FetchFromApi, TimeSpan.FromDays(7));
}
public async Task<Set?> GetSet(string set, string language)
{
var cacheKey = $"tcgdex_set_{set.ToLower()}_{language.ToLower()}";
async Task<Set?> FetchFromApi()
{
var res = await _httpClient.GetFromJsonAsync<Set>($"{language}/sets/{set}");
return res;
}
return await _cacheService.GetOrCreateAsync(cacheKey, FetchFromApi, TimeSpan.FromDays(7));
}
public async Task<Card?> GetCard(string set, int id, string language)
{
var cacheKey = $"tcgdex_card_{set.ToLower()}_{language.ToLower()}";
async Task<Card?> FetchFromApi()
{
var res = await _httpClient.GetFromJsonAsync<Card?>($"{language}/cards/{set}-{id}");
return res;
}
return await _cacheService.GetOrCreateAsync(cacheKey, FetchFromApi, TimeSpan.FromDays(7));
}
public async Task<Tuple<string, byte[]>?> DownloadCardImage(Card card, string language)
{
var split = card.Id!.Split("-");
var filePath = Path.Combine(_baseFolder, split[0], language,$"{split[1]}.webp");
if (File.Exists(filePath))
{
var res = await File.ReadAllBytesAsync(filePath);
return Tuple.Create(filePath, res);
}
await _downloadSemaphore.WaitAsync();
try
{
var res = await _httpClient.GetByteArrayAsync(card.Image + "/high.webp");
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
await File.WriteAllBytesAsync(filePath, res);
await Task.Delay(200);
return Tuple.Create(filePath, res);
}
finally
{
_downloadSemaphore.Release();
}
}
}
@@ -0,0 +1,46 @@
using OakArchive.Entities.Enums;
namespace OakArchive.Util.Extensions;
public static class LanguageExtensions
{
public static string ToApiCode(this Language lang)
{
return lang switch
{
Language.Es_Mx => "es-mx",
Language.Pt_Br => "pt-br",
Language.Pt_Pt => "pt-pt",
Language.Zh_Tw => "zh-tw",
Language.Zh_Cn => "zh-cn",
_ => lang.ToString().ToLower()
};
}
// Ein zusätzliches Feld für deine Avalonia-UI (Anzeigename)
public static string GetDisplayName(this Language lang)
{
return lang switch
{
Language.En => "English",
Language.Fr => "Français",
Language.Es => "Español",
Language.Es_Mx => "Español (México)",
Language.It => "Italiano",
Language.Pt => "Português",
Language.Pt_Br => "Português (Brasil)",
Language.Pt_Pt => "Português (Portugal)",
Language.De => "Deutsch",
Language.Nl => "Nederlands",
Language.Pl => "Polski",
Language.Ru => "Русский",
Language.Ja => "日本語",
Language.Ko => "한국어",
Language.Zh_Tw => "繁體中文 (Taiwan)",
Language.Id => "Bahasa Indonesia",
Language.Th => "ไทย",
Language.Zh_Cn => "简体中文 (China)",
_ => lang.ToString()
};
}
}
+47
View File
@@ -0,0 +1,47 @@
using CoenM.ImageHash.HashAlgorithms;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace OakArchive.Util;
public class PerceptualHashHelper
{
private static readonly PerceptualHash _perceptualHash = new();
private const int TargetWidth = 600;
private const int TargetHeight = 840;
private const int Margin = 15;
public static ulong GenerateHash(byte[] rawImage)
{
using var image = Image.Load<Rgba32>(rawImage);
image.Mutate(x => x.Resize(TargetWidth, TargetHeight));
return _perceptualHash.Hash(image);
}
public static ulong GenerateHashWithMargin(byte[] rawImage)
{
using var image = Image.Load<Rgba32>(rawImage);
const int maxContentWidth = TargetWidth - (2 * Margin);
const int maxContentHeight = TargetHeight - (2 * Margin);
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxContentWidth, maxContentHeight),
Mode = ResizeMode.Max
}));
using var canvas = new Image<Rgba32>(TargetWidth, TargetHeight, Color.Black);
var posX = (TargetWidth - image.Width) / 2;
var posY = (TargetHeight - image.Height) / 2;
canvas.Mutate(x => x.DrawImage(image, new Point(posX, posY), 1f));
return _perceptualHash.Hash(canvas);
}
}
+3 -4
View File
@@ -4,13 +4,12 @@
build:
context: .
dockerfile: OakArchive/Dockerfile
# Falls die DB in einer anderen Datei ist, nimm depends_on raus,
# da Compose nur Services innerhalb derselben Datei überwachen kann.
networks:
- postgres-network
environment:
# WICHTIG: Host muss der 'container_name' oder 'service_name' der DB sein
- CONNECTION_STRING=Host=OmniDB;Database=oak_db;Username=admin;Password=${DB_PASSWORD}
- DefaultConnection=Host=OmniDB;Database=oak_db;Username=admin;Password=${DB_PASSWORD}
volumes:
- C:\Users\larsh\Documents\docker\oakarchive\Cards:/app/Cards
networks:
postgres-network: