diff --git a/app/Http/Controllers/Download/DownloadSongsController.php b/app/Http/Controllers/Download/DownloadSongsController.php index 84801ebe..9ca3b47a 100644 --- a/app/Http/Controllers/Download/DownloadSongsController.php +++ b/app/Http/Controllers/Download/DownloadSongsController.php @@ -8,12 +8,15 @@ use App\Models\Song; use App\Repositories\SongRepository; use App\Services\DownloadService; use Illuminate\Http\Response; +use Illuminate\Support\Collection; class DownloadSongsController extends Controller { public function __invoke(DownloadSongsRequest $request, DownloadService $service, SongRepository $repository) { // Don't use SongRepository::findMany() because it'd have been already catered to the current user. + + /** @var Array|Collection $songs */ $songs = Song::query()->findMany($request->songs); $songs->each(fn ($song) => $this->authorize('download', $song)); diff --git a/app/Models/Podcast.php b/app/Models/Podcast.php index a3372ee8..2fd19d8f 100644 --- a/app/Models/Podcast.php +++ b/app/Models/Podcast.php @@ -18,7 +18,7 @@ use PhanAn\Poddle\Values\CategoryCollection; use PhanAn\Poddle\Values\ChannelMetadata; /** - * @property-read string $id + * @property string $id * @property string $url * @property string $title * @property string $description diff --git a/app/Models/Song.php b/app/Models/Song.php index 15ded62f..55c97a89 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -17,6 +17,7 @@ use App\Values\SongStorageMetadata\S3LambdaMetadata; use App\Values\SongStorageMetadata\SftpMetadata; use App\Values\SongStorageMetadata\SongStorageMetadata; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -31,7 +32,7 @@ use Throwable; /** * @property string $path * @property string $title - * @property Album $album + * @property ?Album $album * @property User $uploader * @property ?Artist $artist * @property ?Artist $album_artist @@ -105,13 +106,12 @@ class Song extends Model public static function query(?PlayableType $type = null, ?User $user = null): SongBuilder { return parent::query() - ->when($type, static function (SongBuilder $query) use ($type): void { - match ($type) { - PlayableType::SONG => $query->whereNull('songs.podcast_id'), - PlayableType::PODCAST_EPISODE => $query->whereNotNull('songs.podcast_id'), - }; + ->when($type, static fn (Builder $query) => match ($type) { // @phpstan-ignore-line phpcs:ignore + PlayableType::SONG => $query->whereNull('songs.podcast_id'), + PlayableType::PODCAST_EPISODE => $query->whereNotNull('songs.podcast_id'), + default => $query, }) - ->when($user, static fn (SongBuilder $query) => $query->forUser($user)); + ->when($user, static fn (SongBuilder $query) => $query->forUser($user)); // @phpstan-ignore-line } public function owner(): BelongsTo diff --git a/app/Policies/EpisodePolicy.php b/app/Policies/EpisodePolicy.php deleted file mode 100644 index 3c62eb76..00000000 --- a/app/Policies/EpisodePolicy.php +++ /dev/null @@ -1,14 +0,0 @@ -subscribedToPodcast($episode->podcast); - } -} diff --git a/app/Services/LicenseService.php b/app/Services/LicenseService.php index 95e71ae7..a3b7d668 100644 --- a/app/Services/LicenseService.php +++ b/app/Services/LicenseService.php @@ -21,8 +21,9 @@ use Throwable; class LicenseService implements LicenseServiceInterface { - public function __construct(private readonly LemonSqueezyConnector $connector, private readonly string $hashSalt) + public function __construct(private readonly LemonSqueezyConnector $connector, private ?string $hashSalt = null) { + $this->hashSalt ??= config('app.key'); } public function activate(string $key): License diff --git a/app/Services/PodcastService.php b/app/Services/PodcastService.php index ca5c0bdc..3a528463 100644 --- a/app/Services/PodcastService.php +++ b/app/Services/PodcastService.php @@ -13,6 +13,7 @@ use App\Repositories\SongRepository; use Carbon\Carbon; use GuzzleHttp\Client; use GuzzleHttp\RedirectMiddleware; +use GuzzleHttp\RequestOptions; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; @@ -56,6 +57,7 @@ class PodcastService $channel = $parser->getChannel(); return DB::transaction(function () use ($url, $podcast, $parser, $channel, $user) { + /** @var Podcast $podcast */ $podcast = Podcast::query()->create([ 'url' => $url, 'title' => $channel->title, @@ -102,8 +104,8 @@ class PodcastService 'last_synced_at' => now(), ]); - $pubDate = $parser->xmlReader->value('rss.channel.pubDate')?->first() - ?? $parser->xmlReader->value('rss.channel.lastBuildDate')?->first(); + $pubDate = $parser->xmlReader->value('rss.channel.pubDate')->first() + ?? $parser->xmlReader->value('rss.channel.lastBuildDate')->first(); if ($pubDate && Carbon::createFromFormat(Carbon::RFC1123, $pubDate)->isBefore($podcast->last_synced_at)) { // The pubDate/lastBuildDate value indicates that there's no new content since last check. @@ -150,7 +152,7 @@ class PodcastService // Since insert() doesn't trigger model events, Scout operations will not be called. // We have to manually update the search index. - Episode::query()->whereIn('id', $ids)->searchable(); + Episode::query()->whereIn('id', $ids)->searchable(); // @phpstan-ignore-line } private function subscribeUserToPodcast(User $user, Podcast $podcast): void @@ -206,16 +208,16 @@ class PodcastService $url = $url->path; } - $client ??= $this->client ?? new Client(); + $client ??= new Client(); try { $response = $client->request($method, $url, [ - 'headers' => [ + RequestOptions::HEADERS => [ 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', // @phpcs-ignore-line 'Origin' => '*', ], - 'http_errors' => false, - 'allow_redirects' => ['track_redirects' => true], + RequestOptions::HTTP_ERRORS => false, + RequestOptions::ALLOW_REDIRECTS => ['track_redirects' => true], ]); $redirects = Arr::wrap($response->getHeader(RedirectMiddleware::HISTORY_HEADER)); diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index b9aa4429..268b59fa 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -52,7 +52,7 @@ class SearchService { try { return $repository->getMany( - ids: $repository->model::search($keywords)->get()->take($count)->pluck('id')->all(), + ids: $repository->model::search($keywords)->get()->take($count)->pluck('id')->all(), // @phpstan-ignore-line preserveOrder: true, ); } catch (Throwable $e) { diff --git a/app/Services/SongService.php b/app/Services/SongService.php index 5c23791b..dae85886 100644 --- a/app/Services/SongService.php +++ b/app/Services/SongService.php @@ -41,7 +41,7 @@ class SongService return collect($ids)->reduce(function (Collection $updated, string $id) use ($data): Collection { optional( Song::query()->with('album.artist')->find($id), - fn (Song $song) => $updated->push($this->updateSong($song, clone $data)) + fn (Song $song) => $updated->push($this->updateSong($song, clone $data)) // @phpstan-ignore-line ); return $updated; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 75df91b1..b9525845 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -29,6 +29,7 @@ parameters: - '#Method App\\Models\\.*::query\(\) should return App\\Builders\\.*Builder but returns Illuminate\\Database\\Eloquent\\Builder#' - '#Parameter \#1 \$callback of method Illuminate\\Support\\Collection::each\(\) expects callable\(Illuminate\\Database\\Eloquent\\Model, int\)#' - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::#' + - '#Unknown parameter \$(type|user) in call to static method App\\Models\\Song::query\(\)#' excludePaths: diff --git a/routes/channels.php b/routes/channels.php index 11c31d91..480801e3 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -1,7 +1,5 @@ (int) $user->id === (int) $id); diff --git a/tests/Feature/ExcerptSearchTest.php b/tests/Feature/ExcerptSearchTest.php index 138c6f8a..7d34fa66 100644 --- a/tests/Feature/ExcerptSearchTest.php +++ b/tests/Feature/ExcerptSearchTest.php @@ -28,7 +28,9 @@ class ExcerptSearchTest extends TestCase Album::factory(4)->create(); $user = create_user(); - $user->subscribeToPodcast(Podcast::factory()->create(['title' => 'Foo Podcast'])); + /** @var Podcast $podcast */ + $podcast = Podcast::factory()->create(['title' => 'Foo Podcast']); + $user->subscribeToPodcast($podcast); $this->getAs('api/search?q=foo', $user) ->assertJsonStructure([ diff --git a/tests/Integration/Services/PlaylistServiceTest.php b/tests/Integration/Services/PlaylistServiceTest.php index 3f589a53..f2c1b94f 100644 --- a/tests/Integration/Services/PlaylistServiceTest.php +++ b/tests/Integration/Services/PlaylistServiceTest.php @@ -212,6 +212,7 @@ class PlaylistServiceTest extends TestCase $playlist = Playlist::factory()->create(); $playlist->addPlayables(Song::factory(3)->create()); + /** @var Podcast $podcast */ $podcast = Podcast::factory()->create(); $episodes = Song::factory(2)->asEpisode()->for($podcast)->create(); diff --git a/tests/Integration/Services/PodcastServiceTest.php b/tests/Integration/Services/PodcastServiceTest.php index 4bfc1ac5..c1317e8e 100644 --- a/tests/Integration/Services/PodcastServiceTest.php +++ b/tests/Integration/Services/PodcastServiceTest.php @@ -1,6 +1,6 @@ create([ 'url' => 'https://example.com/feed.xml', 'title' => 'My Cool Podcast', @@ -79,6 +80,7 @@ class PodcastServiceTest extends TestCase { self::expectException(UserAlreadySubscribedToPodcast::class); + /** @var Podcast $podcast */ $podcast = Podcast::factory()->create([ 'url' => 'https://example.com/feed.xml', ]); @@ -97,6 +99,7 @@ class PodcastServiceTest extends TestCase 'https://example.com/feed.xml' => Http::response(headers: ['Last-Modified' => now()->toRfc1123String()]), ]); + /** @var Podcast $podcast */ $podcast = Podcast::factory()->create([ 'url' => 'https://example.com/feed.xml', 'title' => 'Shall be changed very sad', @@ -116,6 +119,7 @@ class PodcastServiceTest extends TestCase public function testUnsubscribeUserFromPodcast(): void { + /** @var Podcast $podcast */ $podcast = Podcast::factory()->create(); $user = create_user(); $user->subscribeToPodcast($podcast); @@ -127,6 +131,7 @@ class PodcastServiceTest extends TestCase public function testPodcastNotObsoleteIfSyncedRecently(): void { + /** @var Podcast $podcast */ $podcast = Podcast::factory()->create([ 'last_synced_at' => now()->subHours(6), ]); @@ -140,6 +145,7 @@ class PodcastServiceTest extends TestCase 'https://example.com/feed.xml' => Http::response(headers: ['Last-Modified' => now()->toRfc1123String()]), ]); + /** @var Podcast $podcast */ $podcast = Podcast::factory()->create([ 'url' => 'https://example.com/feed.xml', 'last_synced_at' => now()->subDays(1), @@ -150,6 +156,7 @@ class PodcastServiceTest extends TestCase public function testUpdateEpisodeProgress(): void { + /** @var Song $episode */ $episode = Song::factory()->asEpisode()->create(); $user = create_user(); $user->subscribeToPodcast($episode->podcast); diff --git a/tests/Integration/Values/EpisodePlayableTest.php b/tests/Integration/Values/EpisodePlayableTest.php index 886dd644..af1b6298 100644 --- a/tests/Integration/Values/EpisodePlayableTest.php +++ b/tests/Integration/Values/EpisodePlayableTest.php @@ -1,6 +1,6 @@ Http::response('foo'), ]); + /** @var Song $episode */ $episode = Song::factory()->asEpisode()->create([ 'path' => 'https://example.com/episode.mp3', ]);