239 lines
6.2 KiB
Go
239 lines
6.2 KiB
Go
package useCases
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/domain/site"
|
|
"RenewCMS/internal/domain/user"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
type MediaItem struct {
|
|
Image image.Image
|
|
UploadedBy string
|
|
UsedIn int
|
|
Duplicate bool
|
|
|
|
VariantCount int
|
|
VariantSize int64
|
|
}
|
|
|
|
type MediaCounts struct {
|
|
All int
|
|
Unused int
|
|
Duplicates int
|
|
}
|
|
|
|
type MediaPage struct {
|
|
Items []MediaItem
|
|
Total int
|
|
Page int
|
|
Size int
|
|
Counts MediaCounts
|
|
}
|
|
|
|
type ListMediaUseCase struct {
|
|
imageRepository image.Repository
|
|
articleRepository article.Repository
|
|
userRepository user.Repository
|
|
variantFiles image.VariantFileRepository
|
|
sites site.Repository
|
|
authors author.Repository
|
|
}
|
|
|
|
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository, variantFiles image.VariantFileRepository, sites site.Repository, authors author.Repository) *ListMediaUseCase {
|
|
return &ListMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, userRepository: userRepository, variantFiles: variantFiles, sites: sites, authors: authors}
|
|
}
|
|
|
|
func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
|
|
if err := query.Validate(); err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
|
|
usage, err := l.usageCounter()
|
|
if err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
duplicateHashes, err := l.duplicateHashes()
|
|
if err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
counts, err := l.counts(usage)
|
|
if err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
variantTotals, err := l.variantFiles.TotalsBySource()
|
|
if err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
|
|
images, err := l.imageRepository.List(image.SearchQuery{Search: strings.TrimSpace(query.Search), Hash: query.Hash})
|
|
if err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
sortImages(images, query.Sort, variantTotals)
|
|
|
|
matching := make([]image.Image, 0, len(images))
|
|
for _, img := range images {
|
|
if matchesVariants(query.Variants, variantTotals[image.StoredName(img.Path)].Files) && matchesFilter(query.Filter, usage(img), duplicateHashes[img.SHA256] && img.SHA256 != "") {
|
|
matching = append(matching, img)
|
|
}
|
|
}
|
|
|
|
offset := (query.Page - 1) * query.Size
|
|
pageStart := min(offset, len(matching))
|
|
pageEnd := min(pageStart+query.Size, len(matching))
|
|
names := newUploaderNames(l.userRepository)
|
|
items := []MediaItem{}
|
|
for _, img := range matching[pageStart:pageEnd] {
|
|
uploadedBy, err := names.nameOf(img.UploadedByID)
|
|
if err != nil {
|
|
return MediaPage{}, err
|
|
}
|
|
own := variantTotals[image.StoredName(img.Path)]
|
|
items = append(items, MediaItem{
|
|
Image: img,
|
|
UploadedBy: uploadedBy,
|
|
UsedIn: usage(img),
|
|
Duplicate: img.SHA256 != "" && duplicateHashes[img.SHA256],
|
|
VariantCount: own.Files,
|
|
VariantSize: own.Bytes,
|
|
})
|
|
}
|
|
|
|
return MediaPage{Items: items, Total: len(matching), Page: query.Page, Size: query.Size, Counts: counts}, nil
|
|
}
|
|
|
|
func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) {
|
|
articleCount, err := l.articleUsageCount(img)
|
|
if err != nil {
|
|
return MediaItem{}, err
|
|
}
|
|
siteUsage, err := imageUsageCount(l.sites, l.authors)
|
|
if err != nil {
|
|
return MediaItem{}, err
|
|
}
|
|
uploadedBy, err := newUploaderNames(l.userRepository).nameOf(img.UploadedByID)
|
|
if err != nil {
|
|
return MediaItem{}, err
|
|
}
|
|
|
|
duplicate := false
|
|
if img.SHA256 != "" {
|
|
sameContent, err := l.imageRepository.FindByHash(img.SHA256)
|
|
if err != nil {
|
|
return MediaItem{}, err
|
|
}
|
|
duplicate = len(sameContent) > 1
|
|
}
|
|
|
|
rows, err := l.variantFiles.ListBySource(image.StoredName(img.Path))
|
|
if err != nil {
|
|
return MediaItem{}, err
|
|
}
|
|
var variantSize int64
|
|
for _, row := range rows {
|
|
variantSize += row.Size
|
|
}
|
|
|
|
return MediaItem{Image: img, UploadedBy: uploadedBy, UsedIn: articleCount + siteUsage[img.ID], Duplicate: duplicate, VariantCount: len(rows), VariantSize: variantSize}, nil
|
|
}
|
|
|
|
func (l *ListMediaUseCase) articleUsageCount(img image.Image) (int, error) {
|
|
inBody, err := l.articleRepository.FindByBodyContaining(image.StoredName(img.Path))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
referencing, err := l.articleRepository.FindByImage(img.ID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
distinct := map[uint32]bool{}
|
|
for _, found := range slices.Concat(inBody, referencing) {
|
|
distinct[found.ID] = true
|
|
}
|
|
return len(distinct), nil
|
|
}
|
|
|
|
func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
|
|
articles, err := l.articleRepository.FindByFilters(article.Filters{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
siteUsage, err := imageUsageCount(l.sites, l.authors)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func(img image.Image) int {
|
|
used := siteUsage[img.ID]
|
|
for _, a := range articles {
|
|
if articleMentionsImage(a, img) {
|
|
used++
|
|
}
|
|
}
|
|
return used
|
|
}, nil
|
|
}
|
|
|
|
func (l *ListMediaUseCase) duplicateHashes() (map[string]bool, error) {
|
|
duplicates, err := l.imageRepository.ListDuplicates()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hashes := make(map[string]bool, len(duplicates))
|
|
for _, img := range duplicates {
|
|
hashes[img.SHA256] = true
|
|
}
|
|
return hashes, nil
|
|
}
|
|
|
|
func (l *ListMediaUseCase) counts(usage func(image.Image) int) (MediaCounts, error) {
|
|
libraryCounts, err := l.imageRepository.Counts()
|
|
if err != nil {
|
|
return MediaCounts{}, err
|
|
}
|
|
everything, err := l.imageRepository.List(image.SearchQuery{})
|
|
if err != nil {
|
|
return MediaCounts{}, err
|
|
}
|
|
unused := 0
|
|
for _, img := range everything {
|
|
if usage(img) == 0 {
|
|
unused++
|
|
}
|
|
}
|
|
return MediaCounts{All: libraryCounts.All, Unused: unused, Duplicates: libraryCounts.DuplicateGroups}, nil
|
|
}
|
|
|
|
func sortImages(images []image.Image, order image.Sort, totals map[string]image.VariantFileTotals) {
|
|
if order == image.SortTotalSize {
|
|
image.SortImagesByTotalSize(images, totals)
|
|
return
|
|
}
|
|
image.SortImages(images, order)
|
|
}
|
|
|
|
func matchesVariants(filter image.VariantsFilter, variantFiles int) bool {
|
|
switch filter {
|
|
case image.VariantsWith:
|
|
return variantFiles > 0
|
|
case image.VariantsWithout:
|
|
return variantFiles == 0
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func matchesFilter(filter image.Filter, usedIn int, duplicate bool) bool {
|
|
switch filter {
|
|
case image.FilterUnused:
|
|
return usedIn == 0
|
|
case image.FilterDuplicates:
|
|
return duplicate
|
|
default:
|
|
return true
|
|
}
|
|
}
|