Add ability to add media to games. Search media from steamgriddb.com

media
Pat Hartl 2023-11-02 01:24:42 -05:00
parent 8c61a7e3b5
commit 499b0c910a
25 changed files with 2273 additions and 1 deletions

View File

@ -113,7 +113,7 @@ namespace LANCommander.PlaynitePlugin
var games = LANCommander
.GetGames()
.Where(g => g.Archives != null && g.Archives.Count() > 0);
.Where(g => g != null && g.Archives != null && g.Archives.Count() > 0);
foreach (var game in games)
{

View File

@ -0,0 +1,27 @@
<div class="image-picker">
<div class="image-picker-images">
@foreach (var image in Images)
{
<div class="image-picker-image" style="width: @(Size)px; height: @(Size)px">
<input type="radio" id="image-picker-image-@image.Key" checked="@(Value == image.Key)" name="SelectedResult" @onchange="@(() => SelectionChanged(image.Key))" />
<label for="image-picker-image-@image.Key"></label>
<img src="@image.Value" />
</div>
}
</div>
</div>
@code {
[Parameter] public double Size { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Dictionary<string, string> Images { get; set; }
async Task SelectionChanged(string key)
{
Value = key;
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(key);
}
}

View File

@ -0,0 +1,42 @@
@inherits FeedbackComponent<MediaGrabberOptions, MediaGrabberResult>
@using LANCommander.Data.Enums;
@using LANCommander.Models;
@inject IMediaGrabberService MediaGrabberService
<Slider TValue="double" @bind-Value="Size" DefaultValue="200" Min="50" Max="400" />
<ImagePicker Size="Size" Images="Images" ValueChanged="OnImageSelected" />
@code {
[Parameter] public string Search { get; set; }
[Parameter] public MediaType Type { get; set; }
MediaGrabberResult Media { get; set; }
double Size { get; set; } = 200;
IEnumerable<MediaGrabberResult> Results = new List<MediaGrabberResult>();
Dictionary<string, string> Images { get; set; } = new Dictionary<string, string>();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Results = await MediaGrabberService.SearchAsync(Options.Type, Options.Search);
Images = Results.ToDictionary(r => r.Id, r => r.ThumbnailUrl);
StateHasChanged();
}
}
private void OnImageSelected(string key)
{
Media = Results.FirstOrDefault(r => r.Id == key);
}
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await base.OkCancelRefWithResult!.OnOk(Media);
}
}

View File

@ -0,0 +1,56 @@
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Extensions;
using LANCommander.Models;
using LANCommander.SDK;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class MediaController : ControllerBase
{
private readonly MediaService MediaService;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public MediaController(MediaService mediaService)
{
MediaService = mediaService;
}
[HttpGet]
public async Task<IEnumerable<Media>> Get()
{
return await MediaService.Get();
}
[HttpGet("{id}")]
public async Task<Media> Get(Guid id)
{
return await MediaService.Get(id);
}
[AllowAnonymous]
[HttpGet("{id}/Download")]
public async Task<IActionResult> Download(Guid id)
{
try
{
var media = await MediaService.Get(id);
var fs = System.IO.File.OpenRead(MediaService.GetImagePath(media));
return File(fs, "image/png");
}
catch (Exception ex)
{
return NotFound();
}
}
}
}

View File

@ -90,6 +90,11 @@ namespace LANCommander.Data
gr => gr.HasOne<Game>().WithMany().HasForeignKey("GameId")
);
builder.Entity<Game>()
.HasMany(g => g.Media)
.WithOne(m => m.Game)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<User>()
.HasMany(u => u.GameSaves)
.WithOne(gs => gs.User)
@ -146,5 +151,7 @@ namespace LANCommander.Data
public DbSet<ServerConsole>? ServerConsoles { get; set; }
public DbSet<Redistributable>? Redistributables { get; set; }
public DbSet<Media>? Media { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace LANCommander.Data.Enums
{
public enum MediaType
{
Icon,
Cover,
Background
}
}

View File

@ -35,6 +35,7 @@ namespace LANCommander.Data.Models
public virtual ICollection<SavePath>? SavePaths { get; set; }
public virtual ICollection<Server>? Servers { get; set; }
public virtual ICollection<Redistributable>? Redistributables { get; set; }
public virtual ICollection<Media>? Media { get; set; }
public string? ValidKeyRegex { get; set; }
public virtual ICollection<Key>? Keys { get; set; }

View File

@ -0,0 +1,23 @@
using LANCommander.Data.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace LANCommander.Data.Models
{
[Table("Media")]
public class Media : BaseModel
{
public Guid FileId { get; set; }
public MediaType Type { get; set; }
[MaxLength(2048)]
public string SourceUrl { get; set; }
public Guid GameId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(GameId))]
[InverseProperty("Media")]
public virtual Game? Game { get; set; }
}
}

View File

@ -28,6 +28,7 @@
<PackageReference Include="BlazorMonaco" Version="3.1.0" />
<PackageReference Include="ByteSize" Version="2.1.1" />
<PackageReference Include="CoreRCON" Version="5.0.5" />
<PackageReference Include="craftersmine.SteamGridDB.Net" Version="1.1.5" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.5" />
<PackageReference Include="Hangfire.Core" Version="1.8.5" />
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddMediaEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Media",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
FileId = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
SourceUrl = table.Column<string>(type: "TEXT", maxLength: 2048, nullable: false),
GameId = table.Column<Guid>(type: "TEXT", nullable: false),
CreatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedById = table.Column<Guid>(type: "TEXT", nullable: true),
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedById = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Media", x => x.Id);
table.ForeignKey(
name: "FK_Media_AspNetUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Media_AspNetUsers_UpdatedById",
column: x => x.UpdatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Media_Games_GameId",
column: x => x.GameId,
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Media_CreatedById",
table: "Media",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_Media_GameId",
table: "Media",
column: "GameId");
migrationBuilder.CreateIndex(
name: "IX_Media_UpdatedById",
table: "Media",
column: "UpdatedById");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Media");
}
}
}

View File

@ -474,6 +474,49 @@ namespace LANCommander.Migrations
b.ToTable("Keys");
});
modelBuilder.Entity("LANCommander.Data.Models.Media", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("FileId")
.HasColumnType("TEXT");
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<string>("SourceUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("GameId");
b.HasIndex("UpdatedById");
b.ToTable("Media");
});
modelBuilder.Entity("LANCommander.Data.Models.MultiplayerMode", b =>
{
b.Property<Guid>("Id")
@ -1302,6 +1345,29 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.Media", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Media")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("Game");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.MultiplayerMode", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
@ -1520,6 +1586,8 @@ namespace LANCommander.Migrations
b.Navigation("Keys");
b.Navigation("Media");
b.Navigation("MultiplayerModes");
b.Navigation("SavePaths");

View File

@ -0,0 +1,11 @@
using LANCommander.Components.FileManagerComponents;
using LANCommander.Data.Enums;
namespace LANCommander.Models
{
public class MediaGrabberOptions
{
public MediaType Type { get; set; }
public string Search { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using LANCommander.Data.Enums;
namespace LANCommander.Models
{
public class MediaGrabberResult
{
public string Id { get; set; }
public MediaType Type { get; set; }
public string SourceUrl { get; set; }
public string ThumbnailUrl { get; set; }
}
}

View File

@ -18,6 +18,7 @@
public LANCommanderAuthenticationSettings Authentication { get; set; } = new LANCommanderAuthenticationSettings();
public LANCommanderUserSaveSettings UserSaves { get; set; } = new LANCommanderUserSaveSettings();
public LANCommanderArchiveSettings Archives { get; set; } = new LANCommanderArchiveSettings();
public LANCommanderMediaSettings Media { get; set; } = new LANCommanderMediaSettings();
public LANCommanderIPXRelaySettings IPXRelay { get; set; } = new LANCommanderIPXRelaySettings();
}
@ -51,6 +52,12 @@
public string StoragePath { get; set; } = "Uploads";
}
public class LANCommanderMediaSettings
{
public string SteamGridDbApiKey { get; set; } = "";
public string StoragePath { get; set; } = "Media";
}
public class LANCommanderIPXRelaySettings
{
public bool Enabled { get; set; } = false;

View File

@ -0,0 +1,101 @@
@using LANCommander.Data.Enums;
@using LANCommander.Models;
@inject MediaService MediaService
@inject ModalService ModalService
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%" Class="media-editor">
<SpaceItem>
<Table TItem="Media" DataSource="@Values" HidePagination="true" Responsive>
<PropertyColumn Property="p => p.Id" Title="Preview" Width="100px" Align="ColumnAlign.Center">
@if (MediaService.FileExists(context))
{
<Image Width="100px" Src="@($"/api/Media/{context.Id}/Download")" />
}
</PropertyColumn>
<PropertyColumn Property="p => p.Type">
<Select @bind-Value="context.Type" TItem="MediaType" TItemValue="MediaType" DataSource="Enum.GetValues<MediaType>()" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => SearchMedia(context)" Type="@ButtonType.Text" Icon="@IconType.Outline.Search" />
</SpaceItem>
<SpaceItem>
<Popconfirm OnConfirm="() => RemoveMedia(context)" Title="Are you sure you want to delete this media?">
<Button Type="@ButtonType.Text" Danger Icon="@IconType.Outline.Close" />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddMedia" Type="@ButtonType.Primary">Add Media</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public ICollection<Media> Values { get; set; } = new List<Media>();
[Parameter] public EventCallback<ICollection<Media>> ValuesChanged { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public string GameTitle { get; set; }
private async Task AddMedia()
{
if (Values == null)
Values = new List<Media>();
Values.Add(new Media()
{
GameId = GameId
});
}
private async Task SearchMedia(Media media)
{
var modalOptions = new ModalOptions()
{
Title = "Search Media",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select",
};
var grabberOptions = new MediaGrabberOptions()
{
Type = media.Type,
Search = GameTitle
};
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberResult>(modalOptions, grabberOptions);
modalRef.OnOk = async (result) =>
{
modalRef.Config.ConfirmLoading = true;
media.SourceUrl = result.SourceUrl;
media.FileId = await MediaService.DownloadMediaAsync(result.SourceUrl);
await MediaService.Add(media);
Values = MediaService.Get(m => m.GameId == media.GameId).ToList();
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
};
}
private async Task RemoveMedia(Media media)
{
Values.Remove(media);
await MediaService.Delete(media);
}
}

View File

@ -25,6 +25,7 @@
@if (Game != null && Game.Id != Guid.Empty)
{
<MenuItem RouterLink="@($"/Games/{Game.Id}/Media")">Media</MenuItem>
<MenuItem RouterLink="@($"/Games/{Game.Id}/Actions")">Actions</MenuItem>
<MenuItem RouterLink="@($"/Games/{Game.Id}/Multiplayer")">Multiplayer</MenuItem>
<MenuItem RouterLink="@($"/Games/{Game.Id}/SavePaths")">Save Paths</MenuItem>
@ -107,6 +108,10 @@
@if (Game != null && Game.Id != Guid.Empty)
{
<div data-panel="Media">
<MediaEditor @bind-Values="Game.Media" GameId="Game.Id" GameTitle="@Game.Title" />
</div>
<div data-panel="Actions">
<ActionEditor @bind-Actions="Game.Actions" GameId="Game.Id" ArchiveId="@LatestArchiveId" />
</div>

View File

@ -10,6 +10,7 @@ using NLog.Web;
using System.Text;
using Hangfire;
using NLog;
using LANCommander.Services.MediaGrabbers;
namespace LANCommander
{
@ -141,7 +142,9 @@ namespace LANCommander
builder.Services.AddScoped<ServerService>();
builder.Services.AddScoped<ServerConsoleService>();
builder.Services.AddScoped<GameSaveService>();
builder.Services.AddScoped<MediaService>();
builder.Services.AddScoped<RedistributableService>();
builder.Services.AddScoped<IMediaGrabberService, SteamGridDBMediaGrabber>();
builder.Services.AddSingleton<ServerProcessService>();
builder.Services.AddSingleton<IPXRelayService>();
@ -211,6 +214,9 @@ namespace LANCommander
if (!Directory.Exists(settings.UserSaves.StoragePath))
Directory.CreateDirectory(settings.UserSaves.StoragePath);
if (!Directory.Exists(settings.Media.StoragePath))
Directory.CreateDirectory(settings.Media.StoragePath);
if (!Directory.Exists("Snippets"))
Directory.CreateDirectory("Snippets");

View File

@ -0,0 +1,10 @@
using LANCommander.Data.Enums;
using LANCommander.Models;
namespace LANCommander.Services
{
public interface IMediaGrabberService
{
Task<IEnumerable<MediaGrabberResult>> SearchAsync(MediaType type, string keywords);
}
}

View File

@ -0,0 +1,81 @@
using craftersmine.SteamGridDBNet;
using LANCommander.Data.Enums;
using LANCommander.Models;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace LANCommander.Services.MediaGrabbers
{
public class SteamGridDBMediaGrabber : IMediaGrabberService
{
SteamGridDb SteamGridDb { get; set; }
public SteamGridDBMediaGrabber()
{
var settings = SettingService.GetSettings();
SteamGridDb = new SteamGridDb(settings.Media.SteamGridDbApiKey);
}
public async Task<IEnumerable<MediaGrabberResult>> SearchAsync(MediaType type, string keywords)
{
var games = await SteamGridDb.SearchForGamesAsync(keywords);
if (games.Length > 0)
{
var game = games.FirstOrDefault();
switch (type)
{
case MediaType.Icon:
return await GetIconsAsync(game.Id);
case MediaType.Cover:
return await GetCoversAsync(game.Id);
case MediaType.Background:
return await GetBackgroundsAsync(game.Id);
}
}
return new List<MediaGrabberResult>();
}
private async Task<IEnumerable<MediaGrabberResult>> GetIconsAsync(int gameId)
{
var icons = await SteamGridDb.GetIconsByGameIdAsync(gameId);
return icons.Select(i => new MediaGrabberResult()
{
Id = i.Id.ToString(),
Type = MediaType.Icon,
SourceUrl = i.FullImageUrl,
ThumbnailUrl = i.ThumbnailImageUrl
});
}
private async Task<IEnumerable<MediaGrabberResult>> GetCoversAsync(int gameId)
{
var covers = await SteamGridDb.GetGridsByGameIdAsync(gameId);
return covers.Select(c => new MediaGrabberResult()
{
Id = c.Id.ToString(),
Type = MediaType.Cover,
SourceUrl = c.FullImageUrl,
ThumbnailUrl = c.ThumbnailImageUrl
});
}
private async Task<IEnumerable<MediaGrabberResult>> GetBackgroundsAsync(int gameId)
{
var backgrounds = await SteamGridDb.GetHeroesByGameIdAsync(gameId);
return backgrounds.Select(b => new MediaGrabberResult()
{
Id = b.Id.ToString(),
Type = MediaType.Background,
SourceUrl = b.FullImageUrl,
ThumbnailUrl = b.ThumbnailImageUrl
});
}
}
}

View File

@ -0,0 +1,66 @@
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Helpers;
using LANCommander.Models;
namespace LANCommander.Services
{
public class MediaService : BaseDatabaseService<Media>
{
private readonly LANCommanderSettings Settings;
public MediaService(DatabaseContext dbContext, IHttpContextAccessor httpContextAccessor) : base(dbContext, httpContextAccessor)
{
Settings = SettingService.GetSettings();
}
public override Task Delete(Media entity)
{
FileHelpers.DeleteIfExists(GetImagePath(entity));
return base.Delete(entity);
}
public bool FileExists(Media entity)
{
var path = GetImagePath(entity);
return File.Exists(path);
}
public async Task<bool> FileExists(Guid id)
{
var path = await GetImagePath(id);
return File.Exists(path);
}
public async Task<string> GetImagePath(Guid id)
{
var entity = await Get(id);
return GetImagePath(entity);
}
public string GetImagePath(Media entity)
{
return Path.Combine(Settings.Media.StoragePath, entity.FileId.ToString());
}
public async Task<Guid> DownloadMediaAsync(string sourceUrl)
{
var fileId = Guid.NewGuid();
var path = Path.Combine(Settings.Media.StoragePath, fileId.ToString());
using (var http = new HttpClient())
using (var fs = new FileStream(path, FileMode.Create))
{
var response = await http.GetStreamAsync(sourceUrl);
await response.CopyToAsync(fs);
}
return fileId;
}
}
}

View File

@ -173,6 +173,51 @@
background: rgba(255, 255, 255, 0.03);
}
.image-picker-images {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
gap: 16px;
}
.image-picker-image {
position: relative;
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
}
.image-picker-image img {
border: 4px solid transparent;
border-radius: 2px;
max-height: 100%;
max-width: 100%;
}
.image-picker-image input {
display: none;
}
.image-picker-image input + label {
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
cursor: pointer;
}
.image-picker-image input:checked ~ img {
border-color: #1890ff;
}
.media-editor img {
max-width: 100px;
max-height: 100px;
}
@media screen and (min-width: 768px) {
.mobile-menu {
display: none;