Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
6f67918
fix: improve search and harden API services
dotMavriQ Mar 14, 2026
1cc8606
fix: rename date_recorded back to date_finished for books
dotMavriQ Mar 14, 2026
4e46572
fix: cap ComicVine issue pagination at 1000 issues
dotMavriQ Mar 14, 2026
2dcf479
fix: hide irrelevant date sort options based on status filter
dotMavriQ Mar 14, 2026
8db674f
Fix pagination duplicates across all index views
dotMavriQ Mar 19, 2026
ee15052
Add missing WithAccentInsensitiveSearch trait
dotMavriQ Mar 19, 2026
d89d78f
fix: N+1 query in BookImport, update factory column name, add DB sync…
dotMavriQ Mar 21, 2026
77389d8
fix: N+1 query in BookSettings::recacheCovers()
dotMavriQ Mar 21, 2026
a8a749a
fix: add identifier quoting in WithAccentInsensitiveSearch
dotMavriQ Mar 21, 2026
eea99bf
refactor: extract WithSourcePriority trait, delete dead view
dotMavriQ Mar 21, 2026
c587a28
feat: add BookOpenLibrarySearch wizard, fix $query property clash
dotMavriQ Mar 21, 2026
050409c
feat: add Google Books as search source, rename to Discover Books
dotMavriQ Mar 21, 2026
0ef9329
feat: scaffold Playing (Games) category with IGDB Discover
dotMavriQ Mar 21, 2026
6fa1486
feat: add Games category enhancements and Board Games scaffolding
dotMavriQ Mar 22, 2026
0f503f8
feat: add Listening (Music) category with Albums, Concerts, Discogs a…
dotMavriQ Mar 28, 2026
d619e90
fix: update commonmark, phpunit, psysh, pest for security advisories;…
dotMavriQ Mar 28, 2026
a9b193d
refactor: extract WithIndexFiltering trait from 8 index components
dotMavriQ Mar 28, 2026
08ddbc3
Upgrade Saloon 3 → 4 (CVE-2026-33942, CVE-2026-33182, CVE-2026-33183)
dotMavriQ Apr 1, 2026
0ec47b1
Upgrade Laravel 12 → 13, Pest 3 → 4, Tinker 2 → 3
dotMavriQ Apr 1, 2026
a6aed15
Merge TV episode selection into a single-step flow
dotMavriQ Apr 1, 2026
f80a64f
Refactor Board Games for BGG integration
dotMavriQ Apr 1, 2026
2eab91a
Extract WithMetadataEnrichment trait from enrichment components
dotMavriQ Apr 1, 2026
02f0d84
Add proper rate limiting to ComicVine, Jikan, and TMDB connectors
dotMavriQ Apr 1, 2026
3474c32
Improve Discogs search with artist-priority dual query and dedup
dotMavriQ Apr 1, 2026
5c2616d
Fix N+1 query in ImportFromJson cover fetch dispatch
dotMavriQ Apr 1, 2026
4b7c301
Update SEO meta tags and welcome page for full media tracker scope
dotMavriQ Apr 1, 2026
e3d7877
Upgrade Livewire 3 → 4
dotMavriQ Jun 2, 2026
4280b2a
Touch parent TV series timestamp when an episode is saved
dotMavriQ Jun 2, 2026
a002598
dev: make dev DB host port configurable (TEAL_DEV_DB_PORT)
dotMavriQ Jun 2, 2026
73fb56e
chore(quality): add Larastan (PHPStan) at max with baseline
dotMavriQ Jun 2, 2026
c203761
style: enforce Pint (Laravel preset + strict types) repo-wide
dotMavriQ Jun 2, 2026
f3518b4
ci: gate PRs on Pint + PHPStan alongside Pest
dotMavriQ Jun 2, 2026
78c4e3a
dev: pre-push hook + composer quality scripts
dotMavriQ Jun 2, 2026
ca7be87
chore(quality): add Rector (advisory) with composer scripts + CI
dotMavriQ Jun 2, 2026
4a23f81
chore(quality): add type-coverage floor (return 91 / param 79 / prope…
dotMavriQ Jun 2, 2026
ce15879
feat(brand): TEAL seal logo + favicon/PWA asset set
dotMavriQ Jun 2, 2026
5d4ed36
feat(landing): brutalist marketing landing with TEAL brand
dotMavriQ Jun 2, 2026
6765cfb
license: relicense from MIT to AGPL-3.0
dotMavriQ Jun 2, 2026
5c52c54
feat(landing): real app screenshot, copyleft ethos, OG image
dotMavriQ Jun 2, 2026
ea9369d
landing: soften ethos to plain 'human-made & copyleft'
dotMavriQ Jun 2, 2026
7a1b787
landing: use the books library for the screenshot
dotMavriQ Jun 2, 2026
d4a6c98
landing: de-em-dash copy, subtle ethos seal, fix wordmark fit
dotMavriQ Jun 2, 2026
14f6b7e
landing: make the seal-T the dominant glyph in the wordmark
dotMavriQ Jun 2, 2026
b859e11
landing: add yellow Support button with 8-bit heart -> Liberapay
dotMavriQ Jun 2, 2026
35f053c
landing: add 8-bit star to the Star on GitHub button
dotMavriQ Jun 2, 2026
90ae4f0
landing: swap pixel star for a clean bold asterisk on Star on GitHub
dotMavriQ Jun 2, 2026
a676c3a
brand: favicon uses the framed seal (cream tile + ink border + teal)
dotMavriQ Jun 2, 2026
f7d7c35
brand: favicon = the detailed seal on cream
dotMavriQ Jun 2, 2026
dec5ed9
brand: favicon = big seal on the hero light-teal background
dotMavriQ Jun 2, 2026
3b70aa5
brand: favicon seal sized up to fill the tile edge to edge
dotMavriQ Jun 2, 2026
1f4440b
Merge pull request #38 from dotMavriQ/feat/teal-2026-rebrand
dotMavriQ Jun 2, 2026
7ba31e7
docs: document the PSR-12 coding standard and quality gates
dotMavriQ Jun 3, 2026
69fc4ee
perf: prevent lazy loading outside production (N+1 guardrail)
dotMavriQ Jun 3, 2026
74b69c8
refactor(types): fully type DiscogsService, drop 31 baseline entries
dotMavriQ Jun 3, 2026
09cf3f0
refactor(types): fully type ComicVineService, drop 33 baseline entries
dotMavriQ Jun 3, 2026
c26cf31
refactor(types): fully type the remaining Saloon API services
dotMavriQ Jun 3, 2026
8305f3d
refactor(types): fully type the import services & commands (#40)
dotMavriQ Jun 3, 2026
8a7ed87
refactor(types): type the form components (#43)
dotMavriQ Jun 3, 2026
82ec60f
refactor(types): type 3 search-wizard components (#44, partial)
dotMavriQ Jun 3, 2026
600deb7
refactor(types): type remaining search-wizard components (#44)
dotMavriQ Jun 3, 2026
f5d0773
refactor(types): type Book, User, Anime models + Dashboard ripple (#45)
dotMavriQ Jun 3, 2026
de6eb49
refactor(types): type the remaining Eloquent models (#45)
dotMavriQ Jun 3, 2026
d5b3798
refactor(types): type ComicShow & MovieShow + Movie @property docblocks
dotMavriQ Jun 3, 2026
95d34ba
refactor: use #[Layout] attribute so render() returns a typed View
dotMavriQ Jun 3, 2026
092f78b
refactor(types): type the queue jobs (FetchMovie/Book metadata, Impor…
dotMavriQ Jun 4, 2026
1ea48b2
refactor(types): type the import components (Book/Movie/Comic/Anime/J…
dotMavriQ Jun 4, 2026
6942cf7
refactor(types): type ThemeSwitcher, EnrichMovies, FixBookStatuses + …
dotMavriQ Jun 4, 2026
5bb1417
refactor(types): annotate model casts (@property) for enums/dates/arrays
dotMavriQ Jun 4, 2026
645fcaf
refactor(types): type the WithAccentInsensitiveSearch trait
dotMavriQ Jun 4, 2026
3a38f9d
refactor(types): type WithIndexFiltering defaults via overridable met…
dotMavriQ Jun 4, 2026
6f436b1
refactor(types): type all 8 index components (buildQuery, props, filt…
dotMavriQ Jun 4, 2026
cab230b
refactor(types): type Dashboard stats + hub getSubcategories()
dotMavriQ Jun 4, 2026
cdeb1cd
fix(import): parenthesize nested ternary that fatals on PHP 8.0+
dotMavriQ Jun 4, 2026
1463b70
refactor(types): type the metadata-enrichment unit (#41)
dotMavriQ Jun 4, 2026
97d2523
refactor(types): clear scattered model/show/connector tails
dotMavriQ Jun 4, 2026
f241726
refactor(types): clear the long-tail (count guards, configs, connectors)
dotMavriQ Jun 4, 2026
f270fa8
refactor(types): add missing model factories + move TRUSTED_PROXIES t…
dotMavriQ Jun 4, 2026
5efabc4
Merge pull request #46 from dotMavriQ/chore/code-quality
dotMavriQ Jun 4, 2026
c1ad3ce
style: apply curated Rector ruleset; make the advisory CI job green
dotMavriQ Jun 4, 2026
154c32b
Merge pull request #47 from dotMavriQ/chore/rector-advisory-green
dotMavriQ Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
SESSION_COOKIE=teal_session

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
Expand Down
12 changes: 12 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Fast local gate before pushing: style + static analysis.
# Tests run in CI (they need a database); keep this loop quick.
set -euo pipefail

echo "[pre-push] Pint (style)…"
./vendor/bin/pint --test

echo "[pre-push] PHPStan (max, baseline-gated)…"
php -d memory_limit=2G vendor/bin/phpstan analyse --no-progress

echo "[pre-push] ok — pushing."
47 changes: 44 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,54 @@
name: Tests
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
laravel-tests:
quality:
name: Quality (Pint + PHPStan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, iconv, intl, pdo_pgsql
coverage: none
tools: composer:v2
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install dependencies
run: composer install -q --no-ansi --no-interaction --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Pint (style check)
run: vendor/bin/pint --test
- name: PHPStan (max, baseline-gated)
run: vendor/bin/phpstan analyse --no-progress --memory-limit=2G

rector:
name: Rector (advisory)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, iconv, intl, pdo_pgsql
coverage: none
tools: composer:v2
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install dependencies
run: composer install -q --no-ansi --no-interaction --no-progress --prefer-dist
- name: Rector (dry-run, advisory)
run: vendor/bin/rector process --dry-run --no-progress-bar

tests:
name: Tests (Pest)
runs-on: ubuntu-latest

services:
Expand Down
671 changes: 671 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,32 @@ COMIC_VINE_API_KEY=your_key

Book metadata (OpenLibrary) and anime metadata (Jikan/MAL) work without API keys.

## Development

### Coding standard

The codebase follows **PSR-12** (and, by extension, PSR-1 for basic style and PSR-4 for autoloading). Style is enforced with [Laravel Pint](https://laravel.com/docs/pint) using the `laravel` preset — a PSR-12 superset that adds Laravel idioms — configured in `pint.json`. Every PHP file declares `strict_types=1`.

```bash
composer lint # check formatting (pint --test), no changes
./vendor/bin/pint # apply formatting
```

### Static analysis

[PHPStan](https://phpstan.org/) via [Larastan](https://github.com/larastan/larastan) runs at `level: max`, configured in `phpstan.neon`. Existing findings are captured in `phpstan-baseline.neon`; new code must not add to the baseline. Declared-type coverage floors (return/param/property) ratchet up over time and cannot regress.

```bash
composer stan # phpstan analyse
composer quality # lint + stan
```

A `pre-push` hook (`.githooks/pre-push`, enabled via `composer hooks`) runs Pint and PHPStan before every push. Tests run in CI.

```bash
composer test # Pest suite (needs the teal_test database)
```

## License

MIT
74 changes: 43 additions & 31 deletions app/Console/Commands/EnrichMovies.php
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Enums\WatchingStatus;
use App\Models\Movie;
use App\Services\TmdbService;
use App\Services\TraktService;
use Illuminate\Console\Command;
use Illuminate\Support\Sleep;

class EnrichMovies extends Command
{
protected $signature = 'app:enrich-movies {--limit=50}';

protected $description = 'Enrich movies and TV shows with metadata from TMDB and Trakt';

public function handle(TmdbService $tmdb, TraktService $trakt)
public function handle(TmdbService $tmdb, TraktService $trakt): int
{
$limit = (int) $this->option('limit');

$movies = Movie::whereNotNull('imdb_id')
->where('status', \App\Enums\WatchingStatus::Watchlist->value)
->where(function($q) {
->where('status', WatchingStatus::Watchlist->value)
->where(function ($q): void {
$q->whereNull('poster_url')
->orWhereNull('description');
->orWhereNull('description');
})
->take($limit)
->get();

if ($movies->isEmpty()) {
$this->info("No movies need enrichment.");
return;
$this->info('No movies need enrichment.');

return self::SUCCESS;
}

$this->info("Enriching {$movies->count()} items...");

foreach ($movies as $movie) {
$this->line("Processing: {$movie->title} ({$movie->imdb_id})");

$data = $tmdb->findByImdbId($movie->imdb_id);

if (!$data) {
$this->warn(" TMDB miss, trying Trakt...");
$data = $trakt->findByImdbId($movie->imdb_id);
$imdbId = $movie->imdb_id;

if (! is_string($imdbId)) {
continue;
}

$this->line("Processing: {$movie->title} ({$imdbId})");

$data = $tmdb->findByImdbId($imdbId);

if (! $data) {
$this->warn(' TMDB miss, trying Trakt...');
$data = $trakt->findByImdbId($imdbId);
}

if ($data) {
Expand All @@ -53,30 +65,30 @@ public function handle(TmdbService $tmdb, TraktService $trakt)
'metadata_fetched_at' => now(),
]);

if (!empty($updates)) {
$movie->update($updates);
$this->info(" Updated metadata.");

// If this is a show name, propagate the poster to episodes
if (($movie->title_type === 'TV Series' || $movie->title_type === 'TV Mini Series') && $movie->poster_url) {
Movie::propagateShowPoster(
$movie->user_id,
$movie->title,
$movie->title,
$movie->poster_url,
$movie->title
);
}
$movie->update($updates);
$this->info(' Updated metadata.');

// If this is a show name, propagate the poster to episodes
if (($movie->title_type === 'TV Series' || $movie->title_type === 'TV Mini Series') && $movie->poster_url) {
Movie::propagateShowPoster(
$movie->user_id,
$movie->title,
$movie->title,
$movie->poster_url,
$movie->title
);
}
} else {
$this->error(" No metadata found for {$movie->imdb_id}");
$this->error(" No metadata found for {$imdbId}");
$movie->update(['metadata_fetched_at' => now()]);
}

// Simple rate limit protection (4 requests per second)
usleep(250000);
Sleep::usleep(250000);
}

$this->info("Enrichment complete.");
$this->info('Enrichment complete.');

return self::SUCCESS;
}
}
18 changes: 11 additions & 7 deletions app/Console/Commands/FixBookStatuses.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class FixBookStatuses extends Command

protected $description = 'Fix book statuses and optionally extract shelves from the shelves field';

/** @var list<string> */
private array $statusKeywords = ['read', 'to-read', 'currently-reading', 'want-to-read', 'reading'];

public function handle(): int
Expand All @@ -27,11 +28,11 @@ public function handle(): int
$total = Book::whereNotNull('shelves')->count();
$this->info("Processing {$total} books with shelf data...");

Book::whereNotNull('shelves')->with('user')->each(function (Book $book) use ($extractShelves, &$statusUpdated, &$shelvesCreated, &$booksWithShelves) {
$shelfParts = array_map('trim', explode(',', $book->shelves ?? ''));
Book::whereNotNull('shelves')->with('user')->each(function (Book $book) use ($extractShelves, &$statusUpdated, &$shelvesCreated, &$booksWithShelves): void {
$shelfParts = array_map(trim(...), explode(',', $book->shelves ?? ''));

// First part is always status
$statusPart = strtolower($shelfParts[0] ?? '');
$statusPart = strtolower($shelfParts[0]);

// Determine correct status
if (str_contains($statusPart, 'to-read') || str_contains($statusPart, 'want')) {
Expand All @@ -52,13 +53,16 @@ public function handle(): int
// Extract custom shelves (everything after the first part that isn't a status keyword)
if ($extractShelves && count($shelfParts) > 1) {
$customShelves = [];
$counter = count($shelfParts);

for ($i = 1; $i < count($shelfParts); $i++) {
for ($i = 1; $i < $counter; $i++) {
$shelfName = trim($shelfParts[$i]);
$shelfLower = strtolower($shelfName);

// Skip if it's a status keyword
if (empty($shelfName) || in_array($shelfLower, $this->statusKeywords)) {
if (empty($shelfName)) {
continue;
}
if (in_array($shelfLower, $this->statusKeywords)) {
continue;
}

Expand All @@ -67,7 +71,7 @@ public function handle(): int
$shelvesCreated++;
}

if (! empty($customShelves)) {
if ($customShelves !== []) {
$book->bookShelves()->syncWithoutDetaching($customShelves);
$booksWithShelves++;
}
Expand Down
22 changes: 16 additions & 6 deletions app/Console/Commands/ImportGoodreads.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

namespace App\Console\Commands;

use App\Services\JsonImportService;
use App\Models\User;
use App\Services\JsonImportService;
use Exception;
use Illuminate\Console\Command;

class ImportGoodreads extends Command
Expand All @@ -19,23 +20,31 @@ public function handle(): int
$userId = (int) $this->argument('user_id');
$user = User::find($userId);

if (!$user) {
if (! $user) {
$this->error("User with ID {$userId} not found");

return 1;
}

$filePath = base_path('goodreads.txt');

if (!file_exists($filePath)) {
if (! file_exists($filePath)) {
$this->error("File not found: {$filePath}");

return 1;
}

$this->info('Reading goodreads.txt...');
$content = file_get_contents($filePath);

if ($content === false) {
$this->error("Could not read: {$filePath}");

return 1;
}

try {
$service = new JsonImportService();
$service = new JsonImportService;
$books = $service->parseJson($content);

$this->info("Parsed {$books->count()} books");
Expand Down Expand Up @@ -68,8 +77,9 @@ public function handle(): int
$this->info("✓ No changes needed: {$skipped} books");

return 0;
} catch (\Exception $e) {
$this->error('Import failed: ' . $e->getMessage());
} catch (Exception $e) {
$this->error('Import failed: '.$e->getMessage());

return 1;
}
}
Expand Down
Loading
Loading