Compare commits

..

4 Commits

Author SHA1 Message Date
cebd1bd65a 💄 Optimized file analyze 2025-06-26 19:21:26 +08:00
da58e10d88 Ranked posts 2025-06-26 19:17:28 +08:00
d492c9ce1f Realm tags and discovery 2025-06-26 19:00:55 +08:00
f170793928 💄 Optimized web articles 2025-06-26 18:34:51 +08:00
20 changed files with 4428 additions and 32 deletions

View File

@ -1,19 +1,43 @@
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Publisher;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DysonNetwork.Sphere.Activity;
public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps)
public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps, DiscoveryService ds)
{
private double CalculateHotRank(Post.Post post, Instant now)
{
var score = post.Upvotes - post.Downvotes;
var postTime = post.PublishedAt ?? post.CreatedAt;
var hours = (now - postTime).TotalHours;
// Add 1 to score to prevent negative results for posts with more downvotes than upvotes
return (score + 1) / Math.Pow(hours + 2, 1.8);
}
public async Task<List<Activity>> GetActivitiesForAnyone(int take, Instant? cursor)
{
var activities = new List<Activity>();
// Crunching up data
var posts = await db.Posts
if (cursor == null)
{
var realms = await ds.GetPublicRealmsAsync(null, null);
if (realms.Count > 0)
{
activities.Add(new DiscoveryActivity("Explore Realms", realms.Cast<object>().ToList()).ToActivity());
}
}
// Fetch a larger batch of recent posts to rank
var postsQuery = db.Posts
.Include(e => e.RepliedPost)
.Include(e => e.ForwardedPost)
.Include(e => e.Categories)
@ -22,8 +46,9 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
.Where(p => cursor == null || p.PublishedAt < cursor)
.OrderByDescending(p => p.PublishedAt)
.FilterWithVisibility(null, [], [], isListing: true)
.Take(take)
.ToListAsync();
.Take(take * 5); // Fetch more posts to have a good pool for ranking
var posts = await postsQuery.ToListAsync();
posts = await ps.LoadPostInfo(posts, null, true);
var postsId = posts.Select(e => e.Id).ToList();
@ -32,8 +57,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
post.ReactionsCount =
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
// Rank and sort
var now = SystemClock.Instance.GetCurrentInstant();
var rankedPosts = posts
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
.OrderByDescending(x => x.Rank)
.Select(x => x.Post)
.Take(take)
.ToList();
// Formatting data
foreach (var post in posts)
foreach (var post in rankedPosts)
activities.Add(post.ToActivity());
if (activities.Count == 0)
@ -53,6 +87,15 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
var userFriends = await rels.ListAccountFriends(currentUser);
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
if (cursor == null)
{
var realms = await ds.GetPublicRealmsAsync(null, null);
if (realms.Count > 0)
{
activities.Add(new DiscoveryActivity("Explore Realms", realms.Cast<object>().ToList()).ToActivity());
}
}
// Get publishers based on filter
var filteredPublishers = filter switch
{
@ -81,7 +124,7 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
// Complete the query with visibility filtering and execute
var posts = await postsQuery
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true)
.Take(take)
.Take(take * 5) // Fetch more posts to have a good pool for ranking
.ToListAsync();
posts = await ps.LoadPostInfo(posts, currentUser, true);
@ -97,8 +140,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
}
// Rank and sort
var now = SystemClock.Instance.GetCurrentInstant();
var rankedPosts = posts
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
.OrderByDescending(x => x.Rank)
.Select(x => x.Post)
.Take(take)
.ToList();
// Formatting data
foreach (var post in posts)
foreach (var post in rankedPosts)
activities.Add(post.ToActivity());
if (activities.Count == 0)

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using NodaTime;
namespace DysonNetwork.Sphere.Activity
{
public class DiscoveryActivity : IActivity
{
public string Title { get; set; }
public List<object> Items { get; set; }
public DiscoveryActivity(string title, List<object> items)
{
Title = title;
Items = items;
}
public Activity ToActivity()
{
var now = SystemClock.Instance.GetCurrentInstant();
return new Activity
{
Id = Guid.NewGuid(),
Type = "discovery",
ResourceIdentifier = "discovery",
Data = this,
CreatedAt = now,
UpdatedAt = now,
};
}
}
}

View File

@ -75,6 +75,8 @@ public class AppDatabase(
public DbSet<Realm.Realm> Realms { get; set; }
public DbSet<RealmMember> RealmMembers { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<RealmTag> RealmTags { get; set; }
public DbSet<ChatRoom> ChatRooms { get; set; }
public DbSet<ChatMember> ChatMembers { get; set; }
@ -230,6 +232,19 @@ public class AppDatabase(
.HasForeignKey(pm => pm.AccountId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RealmTag>()
.HasKey(rt => new { rt.RealmId, rt.TagId });
modelBuilder.Entity<RealmTag>()
.HasOne(rt => rt.Realm)
.WithMany(r => r.RealmTags)
.HasForeignKey(rt => rt.RealmId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RealmTag>()
.HasOne(rt => rt.Tag)
.WithMany(t => t.RealmTags)
.HasForeignKey(rt => rt.TagId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ChatMember>()
.HasKey(pm => new { pm.Id });
modelBuilder.Entity<ChatMember>()

View File

@ -0,0 +1,7 @@
namespace DysonNetwork.Sphere.Connection.WebReader;
public class ScrapedArticle
{
public LinkEmbed LinkEmbed { get; set; } = null!;
public string? Content { get; set; }
}

View File

@ -23,6 +23,11 @@ public class WebArticle : ModelBase
public WebFeed Feed { get; set; } = null!;
}
public class WebFeedConfig
{
public bool ScrapPage { get; set; }
}
public class WebFeed : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
@ -31,6 +36,7 @@ public class WebFeed : ModelBase
[MaxLength(8192)] public string? Description { get; set; }
[Column(TypeName = "jsonb")] public LinkEmbed? Preview { get; set; }
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
public Guid PublisherId { get; set; }
public Publisher.Publisher Publisher { get; set; } = null!;

View File

@ -1,13 +1,15 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Permission;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Connection.WebReader;
[Authorize]
[ApiController]
[Route("feeds")]
public class WebFeedController(WebFeedService webFeedService) : ControllerBase
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase
{
public class CreateWebFeedRequest
{
@ -30,4 +32,31 @@ public class WebFeedController(WebFeedService webFeedService) : ControllerBase
var feed = await webFeedService.CreateWebFeedAsync(request, User);
return Ok(feed);
}
[HttpPost("scrape/{feedId}")]
[RequiredPermission("maintenance", "web-feeds")]
public async Task<ActionResult> ScrapeFeed(Guid feedId)
{
var feed = await database.Set<WebFeed>().FindAsync(feedId);
if (feed == null)
{
return NotFound();
}
await webFeedService.ScrapeFeedAsync(feed);
return Ok();
}
[HttpPost("scrape-all")]
[RequiredPermission("maintenance", "web-feeds")]
public async Task<ActionResult> ScrapeAllFeeds()
{
var feeds = await database.Set<WebFeed>().ToListAsync();
foreach (var feed in feeds)
{
await webFeedService.ScrapeFeedAsync(feed);
}
return Ok();
}
}

View File

@ -11,10 +11,12 @@ public class WebFeedService(
AppDatabase database,
IHttpClientFactory httpClientFactory,
ILogger<WebFeedService> logger,
AccountService accountService
AccountService accountService,
WebReaderService webReaderService
)
{
public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest dto, ClaimsPrincipal claims)
public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest request,
ClaimsPrincipal claims)
{
if (claims.Identity?.Name == null)
{
@ -29,9 +31,9 @@ public class WebFeedService(
var feed = new WebFeed
{
Url = dto.Url,
Title = dto.Title,
Description = dto.Description,
Url = request.Url,
Title = request.Title,
Description = request.Description,
PublisherId = account.Id,
};
@ -73,14 +75,29 @@ public class WebFeedService(
continue;
}
var content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text;
LinkEmbed preview;
if (feed.Config.ScrapPage)
{
var scrapedArticle = await webReaderService.ScrapeArticleAsync(itemUrl, cancellationToken);
preview = scrapedArticle.LinkEmbed;
content = scrapedArticle.Content;
}
else
{
preview = await webReaderService.GetLinkPreviewAsync(itemUrl, cancellationToken);
}
var newArticle = new WebArticle
{
FeedId = feed.Id,
Title = item.Title.Text,
Url = itemUrl,
Author = item.Authors.FirstOrDefault()?.Name,
Content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text,
Content = content,
PublishedAt = item.PublishDate.UtcDateTime,
Preview = preview,
};
database.Set<WebArticle>().Add(newArticle);

View File

@ -2,6 +2,7 @@ using System.Globalization;
using AngleSharp;
using AngleSharp.Dom;
using DysonNetwork.Sphere.Storage;
using HtmlAgilityPack;
namespace DysonNetwork.Sphere.Connection.WebReader;
@ -17,6 +18,30 @@ public class WebReaderService(
private const string LinkPreviewCachePrefix = "scrap:preview:";
private const string LinkPreviewCacheGroup = "scrap:preview";
public async Task<ScrapedArticle> ScrapeArticleAsync(string url, CancellationToken cancellationToken = default)
{
var linkEmbed = await GetLinkPreviewAsync(url, cancellationToken);
var content = await GetArticleContentAsync(url, cancellationToken);
return new ScrapedArticle
{
LinkEmbed = linkEmbed,
Content = content
};
}
private async Task<string?> GetArticleContentAsync(string url, CancellationToken cancellationToken)
{
var httpClient = httpClientFactory.CreateClient("WebReader");
var response = await httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var html = await response.Content.ReadAsStringAsync(cancellationToken);
var doc = new HtmlDocument();
doc.LoadHtml(html);
var articleNode = doc.DocumentNode.SelectSingleNode("//article");
return articleNode?.InnerHtml;
}
/// <summary>
/// Generate a link preview embed from a URL
/// </summary>

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using DysonNetwork.Sphere.Realm;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Discovery;
[ApiController]
[Route("discovery")]
public class DiscoveryController(DiscoveryService discoveryService) : ControllerBase
{
[HttpGet("realms")]
public Task<List<Realm.Realm>> GetPublicRealms([FromQuery] string? query, [FromQuery] List<string>? tags)
{
return discoveryService.GetPublicRealmsAsync(query, tags);
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Realm;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Discovery;
public class DiscoveryService(AppDatabase appDatabase)
{
public Task<List<Realm.Realm>> GetPublicRealmsAsync(string? query, List<string>? tags)
{
var realmsQuery = appDatabase.Realms
.Where(r => r.IsPublic);
if (!string.IsNullOrEmpty(query))
{
realmsQuery = realmsQuery.Where(r => r.Name.Contains(query) || r.Description.Contains(query));
}
if (tags != null && tags.Count > 0)
{
realmsQuery = realmsQuery.Where(r => r.RealmTags.Any(rt => tags.Contains(rt.Tag.Name)));
}
return realmsQuery.ToListAsync();
}
}

View File

@ -23,6 +23,7 @@
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
<PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
@ -83,6 +84,7 @@
<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="Discovery\" />
</ItemGroup>
<ItemGroup>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
using System;
using DysonNetwork.Sphere.Connection.WebReader;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class AddRealmTags : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<WebFeedConfig>(
name: "config",
table: "web_feeds",
type: "jsonb",
nullable: false);
migrationBuilder.CreateTable(
name: "tags",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_tags", x => x.id);
});
migrationBuilder.CreateTable(
name: "realm_tags",
columns: table => new
{
realm_id = table.Column<Guid>(type: "uuid", nullable: false),
tag_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_realm_tags", x => new { x.realm_id, x.tag_id });
table.ForeignKey(
name: "fk_realm_tags_realms_realm_id",
column: x => x.realm_id,
principalTable: "realms",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_realm_tags_tags_tag_id",
column: x => x.tag_id,
principalTable: "tags",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_realm_tags_tag_id",
table: "realm_tags",
column: "tag_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "realm_tags");
migrationBuilder.DropTable(
name: "tags");
migrationBuilder.DropColumn(
name: "config",
table: "web_feeds");
}
}
}

View File

@ -1438,6 +1438,11 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<WebFeedConfig>("Config")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("config");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
@ -2359,6 +2364,68 @@ namespace DysonNetwork.Sphere.Migrations
b.ToTable("realm_members", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmTag", b =>
{
b.Property<Guid>("RealmId")
.HasColumnType("uuid")
.HasColumnName("realm_id");
b.Property<Guid>("TagId")
.HasColumnType("uuid")
.HasColumnName("tag_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("RealmId", "TagId")
.HasName("pk_realm_tags");
b.HasIndex("TagId")
.HasDatabaseName("ix_realm_tags_tag_id");
b.ToTable("realm_tags", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("name");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_tags");
b.ToTable("tags", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b =>
{
b.Property<Guid>("Id")
@ -3573,6 +3640,27 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Realm");
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmTag", b =>
{
b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm")
.WithMany("RealmTags")
.HasForeignKey("RealmId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_realm_tags_realms_realm_id");
b.HasOne("DysonNetwork.Sphere.Realm.Tag", "Tag")
.WithMany("RealmTags")
.HasForeignKey("TagId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_realm_tags_tags_tag_id");
b.Navigation("Realm");
b.Navigation("Tag");
});
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b =>
{
b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack")
@ -3837,6 +3925,13 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("ChatRooms");
b.Navigation("Members");
b.Navigation("RealmTags");
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b =>
{
b.Navigation("RealmTags");
});
modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b =>

View File

@ -29,6 +29,7 @@ public class Realm : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
[JsonIgnore] public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;

View File

@ -0,0 +1,12 @@
using System;
namespace DysonNetwork.Sphere.Realm;
public class RealmTag : ModelBase
{
public Guid RealmId { get; set; }
public Realm Realm { get; set; } = null!;
public Guid TagId { get; set; }
public Tag Tag { get; set; } = null!;
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace DysonNetwork.Sphere.Realm;
public class Tag : ModelBase
{
public Guid Id { get; set; }
[MaxLength(64)]
public string Name { get; set; } = string.Empty;
public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
}

View File

@ -25,6 +25,7 @@ using StackExchange.Redis;
using System.Text.Json;
using System.Threading.RateLimiting;
using DysonNetwork.Sphere.Connection.WebReader;
using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Safety;
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
using tusdotnet.Stores;
@ -227,6 +228,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<WebFeedService>();
services.AddScoped<AfdianPaymentHandler>();
services.AddScoped<SafetyService>();
services.AddScoped<DiscoveryService>();
return services;
}

View File

@ -1,13 +1,12 @@
using System.Globalization;
using FFMpegCore;
using System.Security.Cryptography;
using AngleSharp.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Minio;
using Minio.DataModel.Args;
using NetVips;
using NodaTime;
using Quartz;
using tusdotnet.Stores;
namespace DysonNetwork.Sphere.Storage;
@ -54,7 +53,7 @@ public class FileService(
private static readonly string TempFilePrefix = "dyn-cloudfile";
private static readonly string[] AnimatedImageTypes =
new[] { "image/gif", "image/apng", "image/webp", "image/avif" };
["image/gif", "image/apng", "image/webp", "image/avif"];
// The analysis file method no longer will remove the GPS EXIF data
// It should be handled on the client side, and for some specific cases it should be keep
@ -115,6 +114,14 @@ public class FileService(
// Try to get orientation from exif data
var orientation = 1;
var meta = new Dictionary<string, object>
{
["blur"] = blurhash,
["format"] = format,
["width"] = width,
["height"] = height,
["orientation"] = orientation,
};
Dictionary<string, object> exif = [];
foreach (var field in vipsImage.GetFields())
@ -122,10 +129,12 @@ public class FileService(
var value = vipsImage.Get(field);
// Skip GPS-related EXIF fields to remove location data
if (IsGpsExifField(field))
if (IsIgnoredField(field))
continue;
exif.Add(field, value);
if (field.StartsWith("exif-")) exif.Add(field.Replace("exif-", ""), value);
else meta.Add(field, value);
if (field == "orientation") orientation = (int)value;
}
@ -134,16 +143,9 @@ public class FileService(
var aspectRatio = height != 0 ? (double)width / height : 0;
file.FileMeta = new Dictionary<string, object>
{
["blur"] = blurhash,
["format"] = format,
["width"] = width,
["height"] = height,
["orientation"] = orientation,
["ratio"] = aspectRatio,
["exif"] = exif
};
meta["exif"] = exif;
meta["ratio"] = aspectRatio;
file.FileMeta = meta;
}
break;
@ -188,7 +190,7 @@ public class FileService(
{
// Skip compression for animated image types
var animatedMimeTypes = AnimatedImageTypes;
if (animatedMimeTypes.Contains(contentType))
if (Enumerable.Contains(animatedMimeTypes, contentType))
{
logger.LogInformation(
"File {fileId} is an animated image (MIME: {mime}), skipping WebP conversion.", fileId,
@ -541,4 +543,11 @@ public class FileService(
fieldName.Equals(gpsField, StringComparison.OrdinalIgnoreCase) ||
fieldName.StartsWith("gps", StringComparison.OrdinalIgnoreCase));
}
private static bool IsIgnoredField(string fieldName)
{
if (IsGpsExifField(fieldName)) return true;
if (fieldName.EndsWith("-data")) return true;
return false;
}
}

View File

@ -78,6 +78,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3bef61b8a21d4c8e96872ecdd7782fa0e55000_003F7a_003F870020d0_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationFeed_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fad_003Fd26b4d73_003FSyndicationFeed_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATotp_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F48c9d2a1b3c84b32b36ebc6f20a927ea4600_003F7b_003Ff98e5727_003FTotp_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>