Skip to content

Commit caad033

Browse files
authored
Merge branch 'master' into user-card-menu-active
2 parents 3c14bdb + 398b071 commit caad033

File tree

76 files changed

+957
-427
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+957
-427
lines changed

.env.example

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ SLACK_ENDPOINT=https://myconan.net/null/
8383
# S3_AVATAR_REGION=
8484
# S3_AVATAR_BUCKET=
8585
# S3_AVATAR_BASE_URL=
86-
# AVATAR_CACHE_PURGE_PREFIX=
87-
# AVATAR_CACHE_PURGE_METHOD=
88-
# AVATAR_CACHE_PURGE_AUTHORIZATION_KEY=
8986
# DEFAULT_AVATAR=http://localhost/images/layout/avatar-guest@2x.png
9087

9188
# S3_BEATMAPSET_BUCKET=beatmapsets
@@ -94,6 +91,8 @@ SLACK_ENDPOINT=https://myconan.net/null/
9491
# SCREENSHOTS_SHARED_SECRET=1234567890abcd
9592
# SCREENSHOTS_LEGACY_ID_CUTOFF=1
9693

94+
# CACHE_PROXY_PURGE_AUTHORIZATION_KEY=
95+
9796
# QUEUE_DRIVER=
9897
# CAMO_KEY=
9998
# CAMO_PREFIX=
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4+
// See the LICENCE file in the repository root for full licence text.
5+
6+
declare(strict_types=1);
7+
8+
namespace App\Exceptions;
9+
10+
class CacheProxyPurgeException extends \Exception
11+
{
12+
}

app/Http/Controllers/LegacyInterOpController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use App\Exceptions\Handler as ExceptionHandler;
99
use App\Jobs\EsDocument;
1010
use App\Jobs\Notifications\ForumTopicReply;
11-
use App\Jobs\RegenerateBeatmapsetCover;
11+
use App\Jobs\RegenerateBeatmapsetMedia;
1212
use App\Libraries\Chat;
1313
use App\Models\Beatmap;
1414
use App\Models\Beatmapset;
@@ -58,7 +58,7 @@ public function indexBeatmapset($id)
5858
$beatmapset = Beatmapset::withTrashed()->findOrFail($id);
5959

6060
if (!$beatmapset->trashed()) {
61-
$job = (new RegenerateBeatmapsetCover($beatmapset))->onQueue('beatmap_default');
61+
$job = (new RegenerateBeatmapsetMedia($beatmapset))->onQueue('beatmap_default');
6262
$this->dispatch($job);
6363
}
6464

app/Http/Controllers/TagsController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public function __construct()
1818

1919
public function index()
2020
{
21+
priv_check('BeatmapsetAdvancedSearch')->ensureCan();
22+
2123
return [
2224
'tags' => app('tags')->json(),
2325
];
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4+
// See the LICENCE file in the repository root for full licence text.
5+
6+
declare(strict_types=1);
7+
8+
namespace App\Jobs;
9+
10+
use App\Exceptions\BeatmapProcessorException;
11+
use App\Exceptions\SilencedException;
12+
use App\Models\Beatmapset;
13+
use Illuminate\Bus\Queueable;
14+
use Illuminate\Contracts\Queue\ShouldQueue;
15+
use Illuminate\Queue\InteractsWithQueue;
16+
use Illuminate\Queue\SerializesModels;
17+
18+
class RegenerateBeatmapsetMedia implements ShouldQueue
19+
{
20+
use InteractsWithQueue, Queueable, SerializesModels;
21+
22+
/**
23+
* The number of seconds the job can run before timing out.
24+
*
25+
* @var int
26+
*/
27+
public $timeout = 300;
28+
29+
public function __construct(protected Beatmapset $beatmapset)
30+
{
31+
}
32+
33+
public function displayName()
34+
{
35+
return static::class." (Beatmapset {$this->beatmapset->getKey()})";
36+
}
37+
38+
public function handle()
39+
{
40+
$this->runTask(
41+
'regenerate_beatmapset_cover',
42+
fn () => $this->beatmapset->regenerateCovers(),
43+
);
44+
$this->runTask(
45+
'regenerate_beatmapset_preview',
46+
fn () => $this->beatmapset->regenerateAudioPreview(),
47+
);
48+
}
49+
50+
private function runTask(string $task, callable $taskFn): void
51+
{
52+
try {
53+
$taskFn();
54+
datadog_increment("{$task}.ok");
55+
} catch (\Throwable $e) {
56+
datadog_increment("{$task}.error");
57+
log_error(new BeatmapProcessorException(previous: $e), [
58+
'task' => $task,
59+
'id' => $this->beatmapset->getKey(),
60+
]);
61+
throw new SilencedException(previous: $e);
62+
}
63+
}
64+
}

app/Libraries/User/AvatarHelper.php

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public static function set(User $user, ?\SplFileInfo $src): bool
3131
$entry = $id.'_'.time().'.'.$processor->ext();
3232
}
3333

34-
static::purgeCache($id);
34+
cache_proxy_purge(StorageUrl::make(static::DISK, (string) $id));
3535

3636
return $user->update(['user_avatar' => $entry ?? '']);
3737
}
@@ -44,33 +44,4 @@ public static function url(User $user): string
4444
? StorageUrl::make(static::DISK, strtr($value, '_', '?'))
4545
: $GLOBALS['cfg']['osu']['avatar']['default'];
4646
}
47-
48-
private static function purgeCache(int $id): void
49-
{
50-
$prefix = presence($GLOBALS['cfg']['osu']['avatar']['cache_purge_prefix']);
51-
52-
if ($prefix === null) {
53-
return;
54-
}
55-
56-
$method = $GLOBALS['cfg']['osu']['avatar']['cache_purge_method'] ?? 'GET';
57-
$auth = $GLOBALS['cfg']['osu']['avatar']['cache_purge_authorization_key'];
58-
$ctx = [
59-
'http' => [
60-
'method' => $method,
61-
'header' => present($auth) ? "Authorization: {$auth}" : null,
62-
],
63-
];
64-
$suffix = $method === 'GET' ? '?'.time() : ''; // Bypass CloudFlare cache if using GET
65-
$url = "{$prefix}{$id}{$suffix}";
66-
67-
try {
68-
file_get_contents($url, false, stream_context_create($ctx));
69-
} catch (\ErrorException $e) {
70-
// ignores 404 errors, throws everything else
71-
if (!ends_with($e->getMessage(), "404 Not Found\r\n")) {
72-
throw $e;
73-
}
74-
}
75-
}
7647
}

app/Models/Beatmapset.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,9 @@ public function downloadLimited()
482482
return $this->download_disabled || $this->download_disabled_url !== null;
483483
}
484484

485-
public function previewURL()
485+
public function previewUrl(): string
486486
{
487-
return '//b.ppy.sh/preview/'.$this->beatmapset_id.'.mp3';
487+
return "https://b.ppy.sh/preview/{$this->getKey()}.mp3";
488488
}
489489

490490
public function removeCover($targetFilename): void
@@ -500,7 +500,7 @@ public function removeCovers()
500500
// ignore errors
501501
}
502502

503-
$this->update(['cover_updated_at' => $this->freshTimestamp()]);
503+
$this->update(['cover_updated_at' => null]);
504504
}
505505

506506
public function regenerateCovers(?array $sizesToRegenerate = null)
@@ -557,24 +557,37 @@ public function regenerateCovers(?array $sizesToRegenerate = null)
557557
$resized = $processor->resize($this->coverURL('fullsize', $timestamp), $size);
558558
$this->storeCover("$size.jpg", get_stream_filename($resized));
559559
}
560-
}
561560

562-
$this->update(['cover_updated_at' => $this->freshTimestamp()]);
561+
$this->update(['cover_updated_at' => $this->freshTimestamp()]);
562+
}
563563
}
564564

565565
public function regenerateAudioPreview(): bool
566566
{
567+
$storage = storage_disk('beatmapset');
568+
$path = "preview/{$this->getKey()}.mp3";
569+
570+
if ($this->download_disabled) {
571+
$storage->delete($path);
572+
573+
return true;
574+
}
575+
567576
$preview = $this->archive()->generateAudioPreview();
568577

569578
if ($preview === null) {
579+
$storage->delete($path);
580+
570581
return false;
571582
}
572583

573-
return storage_disk('beatmapset')->put(
574-
"preview/{$this->getKey()}.mp3",
575-
$preview,
576-
['Content-Type' => 'audio/ogg'],
577-
);
584+
$ret = $storage->put($path, $preview);
585+
586+
if ($ret) {
587+
cache_proxy_purge($this->previewUrl());
588+
}
589+
590+
return $ret;
578591
}
579592

580593
public function allCoverImagesPresent()

app/Models/BeatmapsetArchive.php

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,27 @@ public static function fetch(Beatmapset $beatmapset): ?static
6868
}
6969
}
7070

71-
private static function convertAudioForPreview(string $audioFile, int $previewTime): ?string
71+
private static function convertAudioForPreview(string $audioFile, ?int $previewTime): ?string
7272
{
7373
$srcFile = tmpfile();
7474
fwrite($srcFile, $audioFile);
7575
$srcFilename = get_stream_filename($srcFile);
76+
$srcFilenameEscaped = escapeshellarg($srcFilename);
7677
$dstFile = tmpfile();
7778
$dstFilename = get_stream_filename($dstFile);
7879

7980
$duration = 10000;
80-
$previewTime = max($previewTime, 0);
81+
if ($previewTime === null || $previewTime < 0) {
82+
$srcDuration = (float) exec(implode(' ', [
83+
'timeout 10s',
84+
'ffprobe',
85+
'-loglevel quiet',
86+
"-i {$srcFilenameEscaped}",
87+
'-show_entries format=duration',
88+
'-of csv=p=0',
89+
]));
90+
$previewTime = 0.4 * $srcDuration * 100;
91+
}
8192

8293
$fadeInExtension = min($previewTime, 100);
8394
$fadeIn = $fadeInExtension + 100;
@@ -87,9 +98,16 @@ private static function convertAudioForPreview(string $audioFile, int $previewTi
8798
$fadeOut = $duration - 1000;
8899

89100
$filter = implode(',', [
90-
// unify output to 44.1kHz stereo
101+
// unify output to stereo
102+
// resample here for correct loudnorm operation
103+
'aresample=resampler=soxr:ochl=stereo',
104+
// TODO: two-pass normalisation
105+
// It requires ffmpeg release after 2026-02-20 for saner output parsing.
106+
// Reference: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/21766
107+
'loudnorm=i=-14',
108+
// unify output to 44.1kHz (loudnorm above resamples to 192kHz)
91109
// note that vorbis doesn't have bit depth
92-
'aresample=osr=44100:ochl=stereo',
110+
'aresample=resampler=soxr:osr=44100',
93111
"afade=t=in:st=0:d={$fadeIn}ms:curve=ipar",
94112
"afade=t=out:st={$fadeOut}ms:d=1000ms:curve=tri",
95113
]);
@@ -101,7 +119,7 @@ private static function convertAudioForPreview(string $audioFile, int $previewTi
101119
'-nostdin',
102120
"-ss {$previewTime}ms",
103121
"-t {$duration}ms",
104-
'-i '.escapeshellarg($srcFilename),
122+
"-i {$srcFilenameEscaped}",
105123
"-af {$filter}",
106124
'-map 0:a', // strip out non-audio streams
107125
'-map_metadata -1', // strip out metadata
@@ -139,7 +157,8 @@ public function osuFileList()
139157
{
140158
return $this->osuFileList ??= array_values(array_unique([
141159
// use db order
142-
...($this->beatmapset?->beatmaps->pluck('filename') ?? []),
160+
// filename column in beatmaps table is nullable
161+
...array_reject_null($this->beatmapset?->beatmaps->pluck('filename') ?? []),
143162
...preg_grep('/\.osu$/i', $this->fileList()),
144163
]));
145164
}
@@ -168,10 +187,10 @@ public function generateAudioPreview(): ?string
168187
$previewTime = $parsedFile->previewTime;
169188
$audioFilename = $parsedFile->audioFilename;
170189

171-
if (isset($audioFilename, $previewTime)) {
190+
if (isset($audioFilename)) {
172191
$audioFile = $this->readFile($audioFilename);
173192

174-
if ($audioFile !== null) {
193+
if ($audioFile !== false) {
175194
return static::convertAudioForPreview($audioFile, $previewTime);
176195
}
177196
}

app/Models/Chat/Channel.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,9 +478,7 @@ public function receiveMessage(User $sender, ?string $content, bool $isAction =
478478

479479
$this->unhide();
480480

481-
if (!$message->isUserCommand()) {
482-
$message->dispatchNotification();
483-
}
481+
$message->dispatchNotification();
484482
new ChatMessageEvent($message)->broadcast(true);
485483
});
486484

app/Models/Chat/Message.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ public function getAttribute($key)
108108

109109
public function dispatchNotification(): void
110110
{
111+
if ($this->isUserCommand()) {
112+
return;
113+
}
114+
111115
$class = match ($this->channel->type) {
112116
Channel::TYPES['announce'] => ChannelAnnouncement::class,
113117
Channel::TYPES['pm'] => ChannelMessage::class,

0 commit comments

Comments
 (0)