Skip to content

Commit

Permalink
Merge pull request #11750 from notbakaneko/feature/beatmapset-show-us…
Browse files Browse the repository at this point in the history
…er-tags

Show custom user tags on beatmapset
  • Loading branch information
nanaya authored Jan 14, 2025
2 parents 8b77760 + bdffa17 commit 05c4a60
Show file tree
Hide file tree
Showing 21 changed files with 178 additions and 84 deletions.
16 changes: 0 additions & 16 deletions app/Http/Controllers/BeatmapTagsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,6 @@ public function __construct()
'destroy',
],
]);

$this->middleware('require-scopes:public', ['only' => 'index']);
}

public function index($beatmapId)
{
$topBeatmapTags = cache_remember_mutexed(
"beatmap_tags:{$beatmapId}",
$GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'],
[],
fn () => Tag::topTags($beatmapId),
);

return [
'beatmap_tags' => $topBeatmapTags,
];
}

public function destroy($beatmapId, $tagId)
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/BeatmapsetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ private function showJson($beatmapset)
'beatmaps.failtimes',
'beatmaps.max_combo',
'beatmaps.owners',
'beatmaps.top_tag_ids',
'converts',
'converts.failtimes',
'converts.owners',
Expand All @@ -415,6 +416,7 @@ private function showJson($beatmapset)
'pack_tags',
'ratings',
'recent_favourites',
'related_tags',
'related_users',
'user',
]);
Expand Down
12 changes: 1 addition & 11 deletions app/Http/Controllers/TagsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@

namespace App\Http\Controllers;

use App\Models\Tag;
use App\Transformers\TagTransformer;

class TagsController extends Controller
{
public function __construct()
Expand All @@ -21,15 +18,8 @@ public function __construct()

public function index()
{
$tags = cache_remember_mutexed(
'tags',
$GLOBALS['cfg']['osu']['tags']['tags_cache_duration'],
[],
fn () => Tag::all(),
);

return [
'tags' => json_collection($tags, new TagTransformer()),
'tags' => app('tags')->json(),
];
}
}
17 changes: 16 additions & 1 deletion app/Models/Beatmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Exceptions\InvariantException;
use App\Jobs\EsDocument;
use App\Libraries\Transactions\AfterCommit;
use App\Traits\Memoizes;
use DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
Expand Down Expand Up @@ -53,7 +54,7 @@
*/
class Beatmap extends Model implements AfterCommit
{
use SoftDeletes;
use Memoizes, SoftDeletes;

public $convert = false;

Expand Down Expand Up @@ -348,6 +349,20 @@ public function status()
return array_search($this->approved, Beatmapset::STATES, true);
}

public function topTagIds()
{
// TODO: Add option to multi query when beatmapset requests all tags for beatmaps?
return $this->memoize(
__FUNCTION__,
fn () => cache_remember_mutexed(
"beatmap_top_tag_ids:{$this->getKey()}",
$GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'],
[],
fn () => $this->beatmapTags()->topTagIds()->limit(50)->get()->toArray(),
),
);
}

private function getDifficultyrating()
{
if ($this->convert) {
Expand Down
16 changes: 16 additions & 0 deletions app/Models/BeatmapTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,34 @@

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;

/**
* @property-read Beatmap $beatmap
* @property int $beatmap_id
* @property \Carbon\Carbon $created_at
* @property int $tag_id
* @property \Carbon\Carbon $updated_at
* @property-read User $user
* @property int $user_id
*/
class BeatmapTag extends Model
{
public $incrementing = false;

protected $primaryKey = ':composite';
protected $primaryKeys = ['beatmap_id', 'tag_id', 'user_id'];

public function scopeTopTagIds(Builder $query)
{
return $query->whereHas('user', fn ($userQuery) => $userQuery->default())
->groupBy('tag_id')
->select('tag_id')
->selectRaw('COUNT(*) as count')
->orderBy('count', 'desc')
->orderBy('tag_id', 'asc');
}

public function beatmap()
{
return $this->belongsTo(Beatmap::class, 'beatmap_id');
Expand Down
16 changes: 0 additions & 16 deletions app/Models/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,4 @@ public function beatmapTags(): HasMany
{
return $this->hasMany(BeatmapTag::class);
}

public static function topTags($beatmapId)
{
return static
::joinRelation(
'beatmapTags',
fn ($q) => $q->where('beatmap_id', $beatmapId)->whereHas('user', fn ($userQuery) => $userQuery->default())
)
->groupBy('id')
->select('id', 'name')
->selectRaw('COUNT(*) as count')
->orderBy('count', 'desc')
->orderBy('id', 'desc')
->limit(50)
->get();
}
}
1 change: 1 addition & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AppServiceProvider extends ServiceProvider
'layout-cache' => Singletons\LayoutCache::class,
'medals' => Singletons\Medals::class,
'smilies' => Singletons\Smilies::class,
'tags' => Singletons\Tags::class,
'user-cover-presets' => Singletons\UserCoverPresets::class,
];

Expand Down
44 changes: 44 additions & 0 deletions app/Singletons/Tags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Singletons;

use App\Models\Tag;
use App\Traits\Memoizes;
use App\Transformers\TagTransformer;
use Illuminate\Support\Collection;

class Tags
{
use Memoizes;

/**
* @return Collection<Tag>
*/
public function all(): Collection
{
return $this->memoize(__FUNCTION__, fn () => Tag::all());
}

public function get(int $id): ?Tag
{
$allById = $this->memoize(
'allById',
fn () => $this->all()->keyBy('id'),
);

return $allById[$id] ?? null;
}

public function json(): array
{
return $this->memoize(
__FUNCTION__,
fn () => json_collection($this->all(), new TagTransformer()),
);
}
}
6 changes: 6 additions & 0 deletions app/Transformers/BeatmapCompactTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class BeatmapCompactTransformer extends TransformerAbstract
'failtimes',
'max_combo',
'owners',
'top_tag_ids',
'user',
];

Expand Down Expand Up @@ -83,6 +84,11 @@ public function includeOwners(Beatmap $beatmap)
]);
}

public function includeTopTagIds(Beatmap $beatmap)
{
return $this->primitive($beatmap->topTagIds());
}

public function includeUser(Beatmap $beatmap)
{
return $this->item(
Expand Down
19 changes: 19 additions & 0 deletions app/Transformers/BeatmapsetCompactTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class BeatmapsetCompactTransformer extends TransformerAbstract
'ratings',
'recent_favourites',
'related_users',
'related_tags',
'user',
];

Expand Down Expand Up @@ -299,6 +300,24 @@ public function includeRelatedUsers(Beatmapset $beatmapset)
return $this->collection($users, new UserCompactTransformer());
}

public function includeRelatedTags(Beatmapset $beatmapset)
{
$beatmaps = $this->beatmaps($beatmapset);
$tagIdSet = new Set($beatmaps->flatMap->topTagIds()->pluck('tag_id'));

$cachedTags = app('tags');
$json = [];

foreach ($tagIdSet as $tagId) {
$tag = $cachedTags->get($tagId);
if ($tag !== null) {
$json[] = $tag;
}
}

return $this->primitive($json);
}

private function beatmaps(Beatmapset $beatmapset, ?Fractal\ParamBag $params = null): EloquentCollection
{
$rel = $beatmapset->trashed() || ($params !== null && $params->get('with_trashed')) ? 'allBeatmaps' : 'beatmaps';
Expand Down
2 changes: 1 addition & 1 deletion database/factories/TagFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TagFactory extends Factory
public function definition(): array
{
return [
'name' => fn () => "Tag {$this->faker->word}",
'name' => fn () => "Tag {$this->faker->unique()->word}",
'description' => fn () => $this->faker->sentence,
];
}
Expand Down
37 changes: 37 additions & 0 deletions resources/js/beatmapsets-show/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json';
import TagJson from 'interfaces/tag-json';
import UserJson from 'interfaces/user-json';
import { keyBy } from 'lodash';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
Expand All @@ -10,6 +11,7 @@ import core from 'osu-core-singleton';
import { find, findDefault, group } from 'utils/beatmap-helper';
import { parse } from 'utils/beatmapset-page-hash';
import { parseJson } from 'utils/json';
import { present } from 'utils/string';
import { currentUrl } from 'utils/turbolinks';

export type ScoreLoadingState = null | 'error' | 'loading' | 'supporter_only' | 'unranked';
Expand All @@ -23,6 +25,8 @@ interface State {
showingNsfwWarning: boolean;
}

type TagJsonWithCount = TagJson & { count: number };

export default class Controller {
@observable hoveredBeatmap: null | BeatmapJsonForBeatmapsetShow = null;
@observable state: State;
Expand Down Expand Up @@ -70,6 +74,39 @@ export default class Controller {
return this.beatmaps.get(this.currentBeatmap.mode) ?? [];
}

@computed
get relatedTags() {
const map = new Map<number, TagJson>();

for (const tag of this.beatmapset.related_tags) {
map.set(tag.id, tag);
}

return map;
}

@computed
get tags() {
const userTags: TagJsonWithCount[] = [];

if (this.currentBeatmap.top_tag_ids != null) {
for (const tagId of this.currentBeatmap.top_tag_ids) {
const maybeTag = this.relatedTags.get(tagId.tag_id);
if (maybeTag == null) continue;

userTags.push({ ...maybeTag, count: tagId.count } );
}
}

return {
mapperTags: this.beatmapset.tags.split(' ').filter(present),
userTags: userTags.sort((a, b) => {
const diff = b.count - a.count;
return diff !== 0 ? diff : a.name.localeCompare(b.name);
}),
};
}

@computed
get usersById() {
return keyBy(this.beatmapset.related_users, 'id') as Partial<Record<number, UserJson>>;
Expand Down
17 changes: 11 additions & 6 deletions resources/js/beatmapsets-show/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ export default class Info extends React.Component<Props> {
return ret;
}

private get tags() {
const tags = this.controller.tags;

return [
...tags.userTags.map((tag) => tag.name),
...tags.mapperTags,
];
}

private get withEditDescription() {
return this.controller.beatmapset.description.bbcode != null;
}
Expand All @@ -84,10 +93,6 @@ export default class Info extends React.Component<Props> {
}

render() {
const tags = this.controller.beatmapset.tags
.split(' ')
.filter(present);

return (
<div className='beatmapset-info u-fancy-scrollbar'>
{this.isEditingDescription &&
Expand Down Expand Up @@ -191,13 +196,13 @@ export default class Info extends React.Component<Props> {
</div>
</div>

{tags.length > 0 &&
{this.tags.length > 0 &&
<div className='beatmapset-info__row beatmapset-info__row--value-overflow'>
<h3 className='beatmapset-info__header'>
{trans('beatmapsets.show.info.tags')}
</h3>
<div className='beatmapset-info__value-overflow'>
{tags.map((tag, i) => (
{this.tags.map((tag, i) => (
<React.Fragment key={`${tag}-${i}`}>
<a
className='beatmapset-info__link'
Expand Down
1 change: 1 addition & 0 deletions resources/js/interfaces/beatmap-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface BeatmapJsonAvailableIncludes {
failtimes: BeatmapFailTimesArray;
max_combo: number;
owners: BeatmapOwnerJson[];
top_tag_ids: { count: number; tag_id: number }[];
user: UserJson;
}

Expand Down
1 change: 1 addition & 0 deletions resources/js/interfaces/beatmapset-extended-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type BeatmapsetJsonForShowIncludes = Required<Pick<BeatmapsetExtendedJson,
| 'language'
| 'ratings'
| 'recent_favourites'
| 'related_tags'
| 'related_users'
| 'user'
>>;
Expand Down
Loading

0 comments on commit 05c4a60

Please sign in to comment.