Skip to content

Commit 0f503f8

Browse files
committed
feat: add Listening (Music) category with Albums, Concerts, Discogs and SetlistFm integration
1 parent 6fa1486 commit 0f503f8

46 files changed

Lines changed: 4542 additions & 86 deletions

Some content is hidden

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

app/Enums/CollectionStatus.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Enums;
6+
7+
enum CollectionStatus: string
8+
{
9+
case Wishlist = 'wishlist';
10+
case Listening = 'listening';
11+
case Listened = 'listened';
12+
case Shelved = 'shelved';
13+
14+
public function label(): string
15+
{
16+
return match ($this) {
17+
self::Wishlist => 'Wishlist',
18+
self::Listening => 'Listening',
19+
self::Listened => 'Listened',
20+
self::Shelved => 'Shelved',
21+
};
22+
}
23+
24+
public function color(): string
25+
{
26+
return match ($this) {
27+
self::Wishlist => 'blue',
28+
self::Listening => 'yellow',
29+
self::Listened => 'green',
30+
self::Shelved => 'gray',
31+
};
32+
}
33+
}

app/Enums/ListeningStatus.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Enums;
6+
7+
enum ListeningStatus: string
8+
{
9+
case WantToGo = 'want_to_go';
10+
case Going = 'going';
11+
case Attended = 'attended';
12+
case Missed = 'missed';
13+
14+
public function label(): string
15+
{
16+
return match ($this) {
17+
self::WantToGo => 'Want to Go',
18+
self::Going => 'Going',
19+
self::Attended => 'Attended',
20+
self::Missed => 'Missed',
21+
};
22+
}
23+
24+
public function color(): string
25+
{
26+
return match ($this) {
27+
self::WantToGo => 'blue',
28+
self::Going => 'yellow',
29+
self::Attended => 'green',
30+
self::Missed => 'gray',
31+
};
32+
}
33+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Livewire\Albums;
6+
7+
use App\Enums\CollectionStatus;
8+
use App\Enums\OwnershipStatus;
9+
use App\Models\Album;
10+
use App\Services\DiscogsService;
11+
use Illuminate\Support\Facades\Auth;
12+
use Livewire\Component;
13+
14+
class AlbumDiscogsSearch extends Component
15+
{
16+
public string $step = 'search';
17+
18+
public string $searchQuery = '';
19+
20+
public array $results = [];
21+
22+
public ?array $selectedRelease = null;
23+
24+
public string $status = 'wishlist';
25+
26+
public string $ownership = 'not_owned';
27+
28+
public ?int $rating = null;
29+
30+
public string $notes = '';
31+
32+
public function search(): void
33+
{
34+
if (empty(trim($this->searchQuery))) {
35+
return;
36+
}
37+
38+
$service = app(DiscogsService::class);
39+
$this->results = $service->search($this->searchQuery);
40+
$this->step = 'results';
41+
}
42+
43+
public function selectRelease(int $index): void
44+
{
45+
$result = $this->results[$index] ?? null;
46+
47+
if (! $result) {
48+
return;
49+
}
50+
51+
$service = app(DiscogsService::class);
52+
53+
$masterId = $result['master_id'] ?? null;
54+
$releaseId = $result['id'] ?? null;
55+
$type = $result['type'] ?? 'master';
56+
57+
if ($type === 'master' && $masterId) {
58+
$details = $service->getMasterDetails($masterId);
59+
} elseif ($releaseId) {
60+
$details = $service->getReleaseDetails($releaseId);
61+
} else {
62+
return;
63+
}
64+
65+
if (! $details) {
66+
session()->flash('error', 'Could not fetch release details.');
67+
68+
return;
69+
}
70+
71+
$this->selectedRelease = $details;
72+
$this->step = 'configure';
73+
}
74+
75+
public function save(): void
76+
{
77+
if (! $this->selectedRelease) {
78+
return;
79+
}
80+
81+
$discogsId = $this->selectedRelease['discogs_id'] ?? null;
82+
$discogsMasterId = $this->selectedRelease['discogs_master_id'] ?? null;
83+
84+
if ($discogsId || $discogsMasterId) {
85+
$existing = Album::where('user_id', Auth::id())
86+
->where(function ($q) use ($discogsId, $discogsMasterId) {
87+
if ($discogsId) {
88+
$q->where('discogs_id', $discogsId);
89+
}
90+
if ($discogsMasterId) {
91+
$q->orWhere('discogs_master_id', $discogsMasterId);
92+
}
93+
})
94+
->exists();
95+
96+
if ($existing) {
97+
session()->flash('error', 'This album is already in your collection.');
98+
99+
return;
100+
}
101+
}
102+
103+
$album = Album::create([
104+
'user_id' => Auth::id(),
105+
'title' => $this->selectedRelease['title'],
106+
'artist' => $this->selectedRelease['artist'],
107+
'genre' => $this->selectedRelease['genre'] ?? [],
108+
'styles' => $this->selectedRelease['styles'] ?? [],
109+
'year' => $this->selectedRelease['year'],
110+
'format' => $this->selectedRelease['format'],
111+
'label' => $this->selectedRelease['label'],
112+
'country' => $this->selectedRelease['country'],
113+
'cover_url' => $this->selectedRelease['cover_url'],
114+
'tracklist' => $this->selectedRelease['tracklist'] ?? [],
115+
'status' => $this->status,
116+
'ownership' => $this->ownership,
117+
'rating' => $this->rating,
118+
'discogs_id' => $this->selectedRelease['discogs_id'],
119+
'discogs_master_id' => $this->selectedRelease['discogs_master_id'],
120+
'notes' => $this->notes ?: null,
121+
]);
122+
123+
session()->flash('message', "{$album->title} added to your collection.");
124+
125+
$this->redirect(route('albums.show', $album));
126+
}
127+
128+
public function back(): void
129+
{
130+
$this->step = match ($this->step) {
131+
'configure' => 'results',
132+
'results' => 'search',
133+
default => 'search',
134+
};
135+
}
136+
137+
public function render()
138+
{
139+
return view('livewire.albums.album-discogs-search', [
140+
'statuses' => CollectionStatus::cases(),
141+
'ownershipStatuses' => OwnershipStatus::cases(),
142+
])->layout('layouts.app');
143+
}
144+
}

app/Livewire/Albums/AlbumForm.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Livewire\Albums;
6+
7+
use App\Enums\CollectionStatus;
8+
use App\Enums\OwnershipStatus;
9+
use App\Models\Album;
10+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
11+
use Illuminate\Support\Facades\Auth;
12+
use Illuminate\Validation\Rule;
13+
use Livewire\Component;
14+
15+
class AlbumForm extends Component
16+
{
17+
use AuthorizesRequests;
18+
19+
public ?Album $album = null;
20+
21+
public string $title = '';
22+
23+
public string $artist = '';
24+
25+
public string $genre = '';
26+
27+
public string $styles = '';
28+
29+
public ?int $year = null;
30+
31+
public string $format = '';
32+
33+
public string $label = '';
34+
35+
public string $country = '';
36+
37+
public string $cover_url = '';
38+
39+
public string $status = 'wishlist';
40+
41+
public string $ownership = 'not_owned';
42+
43+
public ?int $rating = null;
44+
45+
public ?int $discogs_id = null;
46+
47+
public ?int $discogs_master_id = null;
48+
49+
public string $notes = '';
50+
51+
public function mount(?Album $album = null): void
52+
{
53+
if ($album && $album->exists) {
54+
$this->authorize('update', $album);
55+
$this->album = $album;
56+
$this->fill([
57+
'title' => $album->title,
58+
'artist' => $album->artist ?? '',
59+
'genre' => is_array($album->genre) ? implode(', ', $album->genre) : '',
60+
'styles' => is_array($album->styles) ? implode(', ', $album->styles) : '',
61+
'year' => $album->year,
62+
'format' => $album->format ?? '',
63+
'label' => $album->label ?? '',
64+
'country' => $album->country ?? '',
65+
'cover_url' => $album->cover_url ?? '',
66+
'status' => $album->status->value,
67+
'ownership' => $album->ownership?->value ?? 'not_owned',
68+
'rating' => $album->rating,
69+
'discogs_id' => $album->discogs_id,
70+
'discogs_master_id' => $album->discogs_master_id,
71+
'notes' => $album->notes ?? '',
72+
]);
73+
}
74+
}
75+
76+
public function rules(): array
77+
{
78+
return [
79+
'title' => ['required', 'string', 'max:255'],
80+
'artist' => ['nullable', 'string', 'max:255'],
81+
'genre' => ['nullable', 'string', 'max:500'],
82+
'styles' => ['nullable', 'string', 'max:500'],
83+
'year' => ['nullable', 'integer', 'min:1900', 'max:2100'],
84+
'format' => ['nullable', 'string', 'max:255'],
85+
'label' => ['nullable', 'string', 'max:255'],
86+
'country' => ['nullable', 'string', 'max:255'],
87+
'cover_url' => ['nullable', 'url', 'max:2048'],
88+
'status' => ['required', Rule::enum(CollectionStatus::class)],
89+
'ownership' => ['required', Rule::enum(OwnershipStatus::class)],
90+
'rating' => ['nullable', 'integer', 'min:1', 'max:5'],
91+
'discogs_id' => ['nullable', 'integer'],
92+
'discogs_master_id' => ['nullable', 'integer'],
93+
'notes' => ['nullable', 'string', 'max:10000'],
94+
];
95+
}
96+
97+
public function save(): void
98+
{
99+
$validated = $this->validate();
100+
101+
$genreArray = ! empty($validated['genre'])
102+
? array_map('trim', explode(',', $validated['genre']))
103+
: [];
104+
105+
$stylesArray = ! empty($validated['styles'])
106+
? array_map('trim', explode(',', $validated['styles']))
107+
: [];
108+
109+
$data = [
110+
'title' => $validated['title'],
111+
'artist' => $validated['artist'] ?: null,
112+
'genre' => $genreArray,
113+
'styles' => $stylesArray,
114+
'year' => $validated['year'],
115+
'format' => $validated['format'] ?: null,
116+
'label' => $validated['label'] ?: null,
117+
'country' => $validated['country'] ?: null,
118+
'cover_url' => $validated['cover_url'] ?: null,
119+
'status' => $validated['status'],
120+
'ownership' => $validated['ownership'],
121+
'rating' => $validated['rating'],
122+
'discogs_id' => $validated['discogs_id'],
123+
'discogs_master_id' => $validated['discogs_master_id'],
124+
'notes' => $validated['notes'] ?: null,
125+
];
126+
127+
if ($this->album) {
128+
$this->album->update($data);
129+
$message = 'Album updated successfully.';
130+
} else {
131+
$data['user_id'] = Auth::id();
132+
$this->album = Album::create($data);
133+
$message = 'Album created successfully.';
134+
}
135+
136+
session()->flash('message', $message);
137+
138+
$this->redirect(route('albums.show', $this->album));
139+
}
140+
141+
public function isEditing(): bool
142+
{
143+
return $this->album !== null && $this->album->exists;
144+
}
145+
146+
public function render()
147+
{
148+
return view('livewire.albums.album-form', [
149+
'statuses' => CollectionStatus::cases(),
150+
'ownershipStatuses' => OwnershipStatus::cases(),
151+
'isEditing' => $this->isEditing(),
152+
])->layout('layouts.app');
153+
}
154+
}

0 commit comments

Comments
 (0)