Skip to content

Commit d20441f

Browse files
committed
fix: prioritize 'watched' status when sorting by Date Watched (Issue #27)
1 parent b9703fc commit d20441f

3 files changed

Lines changed: 225 additions & 7 deletions

File tree

app/Livewire/Movies/MovieIndex.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,14 @@ public function render()
241241
$query->orderByRaw('year IS NULL')
242242
->orderBy('year', $sortDir);
243243
} elseif ($sortBy === 'date_watched') {
244-
$query->orderBy(DB::raw('COALESCE(date_watched, date_added, updated_at)'), $sortDir);
244+
// Prioritize status 'watched' over others, then use date_watched, falling back to date_added/updated_at
245+
if ($sortDir === 'desc') {
246+
$query->orderByRaw("CASE WHEN status = 'watched' THEN 0 ELSE 1 END")
247+
->orderByRaw('COALESCE(date_watched, date_added, updated_at) DESC');
248+
} else {
249+
$query->orderByRaw("CASE WHEN status = 'watched' THEN 0 ELSE 1 END")
250+
->orderByRaw('COALESCE(date_watched, date_added, updated_at) ASC');
251+
}
245252
} else {
246253
$query->orderBy($sortBy, $sortDir);
247254
}

scripts/backup-db.sh

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env bash
22
# Backup TEAL PostgreSQL database with timestamp
33
# Usage: ./scripts/backup-db.sh [label]
4-
# Example: ./scripts/backup-db.sh pre-test
54

65
set -euo pipefail
76

@@ -14,15 +13,47 @@ FILENAME="teal_${TIMESTAMP}_${LABEL}.sql"
1413

1514
mkdir -p "$BACKUP_DIR"
1615

17-
# Dump from the containerized PostgreSQL
16+
# Try podman exec first, then docker exec, then direct pg_dump
1817
if podman exec teal-db pg_dump -U teal -d teal --no-owner --no-acl > "$BACKUP_DIR/$FILENAME" 2>/dev/null; then
19-
SIZE=$(du -h "$BACKUP_DIR/$FILENAME" | cut -f1)
20-
echo "Backup saved: backups/$FILENAME ($SIZE)"
18+
:
19+
elif docker exec teal-db pg_dump -U teal -d teal --no-owner --no-acl > "$BACKUP_DIR/$FILENAME" 2>/dev/null; then
20+
:
21+
elif PGPASSWORD=secret pg_dump -h 127.0.0.1 -U teal -d teal --no-owner --no-acl > "$BACKUP_DIR/$FILENAME" 2>/dev/null; then
22+
:
2123
else
22-
echo "ERROR: Failed to backup database. Is teal-db container running?" >&2
23-
exit 1
24+
# Last resort: dump each table as INSERT statements via PHP
25+
cd "$PROJECT_DIR"
26+
php artisan tinker --execute="
27+
\$tables = ['users','movies','books','shows','episodes','anime','comics','comic_issues','shelves','book_shelf','migrations','sessions','cache','cache_locks'];
28+
\$out = '';
29+
foreach (\$tables as \$t) {
30+
try {
31+
\$rows = DB::table(\$t)->get();
32+
if (\$rows->isEmpty()) continue;
33+
\$cols = array_keys((array)\$rows[0]);
34+
\$colList = implode(', ', array_map(fn(\$c) => '\"' . \$c . '\"', \$cols));
35+
foreach (\$rows as \$row) {
36+
\$vals = array_map(function(\$v) {
37+
if (\$v === null) return 'NULL';
38+
return \"'\" . str_replace(\"'\", \"''\", (string)\$v) . \"'\";
39+
}, array_values((array)\$row));
40+
\$out .= \"INSERT INTO \\\"\$t\\\" (\$colList) VALUES (\" . implode(', ', \$vals) . \");\n\";
41+
}
42+
} catch (\Exception \$e) {}
43+
}
44+
file_put_contents('$BACKUP_DIR/$FILENAME', \$out);
45+
" 2>/dev/null
46+
47+
if [ ! -s "$BACKUP_DIR/$FILENAME" ]; then
48+
rm -f "$BACKUP_DIR/$FILENAME"
49+
echo "ERROR: Failed to backup database." >&2
50+
exit 1
51+
fi
2452
fi
2553

54+
SIZE=$(du -h "$BACKUP_DIR/$FILENAME" | cut -f1)
55+
echo "Backup saved: backups/$FILENAME ($SIZE)"
56+
2657
# Keep only the 20 most recent backups
2758
cd "$BACKUP_DIR"
2859
ls -t teal_*.sql 2>/dev/null | tail -n +21 | xargs -r rm --

scripts/fix-remaining-episodes.php

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
/**
3+
* Fix remaining ~72 TV Episodes that TMDB couldn't match by IMDb ID.
4+
* Matches by episode title against TMDB season/episode data.
5+
*
6+
* Usage: php artisan tinker scripts/fix-remaining-episodes.php
7+
* OR: cd /home/dotmavriq/Code/TEAL && php artisan tinker < scripts/fix-remaining-episodes.php
8+
*/
9+
10+
use App\Models\Movie;
11+
use App\Services\TmdbService;
12+
use Illuminate\Support\Str;
13+
14+
$tmdb = app(TmdbService::class);
15+
16+
// Shows with known TMDB IDs and the seasons we need
17+
$shows = [
18+
['name' => 'Cardinal', 'tmdb_id' => 67743, 'seasons' => [4]],
19+
['name' => 'Glória', 'tmdb_id' => 109369, 'seasons' => [1]],
20+
['name' => 'Reyka', 'tmdb_id' => 132629, 'seasons' => [1]],
21+
['name' => 'Our Girl', 'tmdb_id' => 61517, 'seasons' => [3]],
22+
['name' => 'Älskade Samir', 'tmdb_id' => 289318, 'seasons' => [1]],
23+
['name' => 'Once Upon a Time in Londongrad', 'tmdb_id' => 203226, 'seasons' => [1]],
24+
['name' => 'Black Market: Dispatches', 'tmdb_id' => 67583, 'seasons' => [1]],
25+
['name' => 'The Power of Nightmares', 'tmdb_id' => 6132, 'seasons' => [1]],
26+
['name' => 'Evil Con Carne', 'tmdb_id' => 4246, 'seasons' => [1, 2]],
27+
['name' => 'NileCity 105.6', 'tmdb_id' => 15232, 'seasons' => [1]],
28+
['name' => 'Uppdrag granskning', 'tmdb_id' => 5848, 'seasons' => [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]],
29+
];
30+
31+
$totalFixed = 0;
32+
$totalSkipped = 0;
33+
34+
foreach ($shows as $show) {
35+
$showName = $show['name'];
36+
$tmdbId = $show['tmdb_id'];
37+
38+
// Find our unmatched episodes for this show
39+
$episodes = Movie::where('title_type', 'TV Episode')
40+
->where(function ($q) use ($showName) {
41+
$q->where('show_name', $showName)
42+
->orWhere('show_name', 'like', $showName . '%')
43+
->orWhere('primary_title', 'like', '%' . $showName . '%')
44+
->orWhere('primary_title', 'like', '%' . Str::ascii($showName) . '%');
45+
})
46+
->where(function ($q) {
47+
$q->whereNull('season_number')
48+
->orWhereNull('episode_number');
49+
})
50+
->get();
51+
52+
if ($episodes->isEmpty()) {
53+
echo "[$showName] No unmatched episodes found in DB, skipping.\n";
54+
continue;
55+
}
56+
57+
echo "[$showName] Found {$episodes->count()} unmatched episodes in DB.\n";
58+
59+
// Fetch all TMDB episodes for relevant seasons
60+
$tmdbEpisodes = [];
61+
foreach ($show['seasons'] as $seasonNum) {
62+
try {
63+
$seasonData = $tmdb->fetchTVSeasonEpisodes($tmdbId, $seasonNum);
64+
if (!empty($seasonData['episodes'])) {
65+
foreach ($seasonData['episodes'] as $ep) {
66+
$tmdbEpisodes[] = [
67+
'name' => $ep['name'] ?? '',
68+
'season' => $seasonNum,
69+
'episode' => $ep['episode_number'] ?? null,
70+
];
71+
}
72+
}
73+
usleep(300000); // rate limit
74+
} catch (\Exception $e) {
75+
echo " Warning: Could not fetch S{$seasonNum}: {$e->getMessage()}\n";
76+
}
77+
}
78+
79+
if (empty($tmdbEpisodes)) {
80+
echo " No TMDB episodes fetched, skipping.\n";
81+
continue;
82+
}
83+
84+
echo " Fetched " . count($tmdbEpisodes) . " TMDB episodes across " . count($show['seasons']) . " season(s).\n";
85+
86+
$fixed = 0;
87+
foreach ($episodes as $ep) {
88+
$ourTitle = trim($ep->primary_title);
89+
$matched = null;
90+
91+
// Strategy 1: Exact title match
92+
foreach ($tmdbEpisodes as $te) {
93+
if (strcasecmp($ourTitle, $te['name']) === 0) {
94+
$matched = $te;
95+
break;
96+
}
97+
}
98+
99+
// Strategy 2: Our title contains TMDB title or vice versa
100+
if (!$matched) {
101+
foreach ($tmdbEpisodes as $te) {
102+
if (empty($te['name'])) continue;
103+
if (Str::contains(strtolower($ourTitle), strtolower($te['name'])) ||
104+
Str::contains(strtolower($te['name']), strtolower($ourTitle))) {
105+
$matched = $te;
106+
break;
107+
}
108+
}
109+
}
110+
111+
// Strategy 3: Parse "Episode #X.Y" format from our title
112+
if (!$matched && preg_match('/Episode\s*#?(\d+)\.(\d+)/i', $ourTitle, $m)) {
113+
$parsedSeason = (int)$m[1];
114+
$parsedEp = (int)$m[2];
115+
foreach ($tmdbEpisodes as $te) {
116+
if ($te['season'] === $parsedSeason && $te['episode'] === $parsedEp) {
117+
$matched = $te;
118+
break;
119+
}
120+
}
121+
}
122+
123+
// Strategy 4: Levenshtein distance (fuzzy match, threshold 3)
124+
if (!$matched) {
125+
$bestDist = 999;
126+
$bestMatch = null;
127+
foreach ($tmdbEpisodes as $te) {
128+
if (empty($te['name'])) continue;
129+
$dist = levenshtein(strtolower($ourTitle), strtolower($te['name']));
130+
if ($dist < $bestDist && $dist <= 3) {
131+
$bestDist = $dist;
132+
$bestMatch = $te;
133+
}
134+
}
135+
if ($bestMatch) {
136+
$matched = $bestMatch;
137+
}
138+
}
139+
140+
if ($matched) {
141+
$ep->season_number = $matched['season'];
142+
$ep->episode_number = $matched['episode'];
143+
if (empty($ep->show_name)) {
144+
$ep->show_name = $showName;
145+
}
146+
$ep->save();
147+
$fixed++;
148+
echo " FIXED: \"{$ourTitle}\" -> S{$matched['season']}E{$matched['episode']}\n";
149+
} else {
150+
$totalSkipped++;
151+
echo " SKIP: \"{$ourTitle}\" - no TMDB match\n";
152+
}
153+
}
154+
155+
$totalFixed += $fixed;
156+
echo " => Fixed {$fixed}/{$episodes->count()} episodes.\n\n";
157+
}
158+
159+
// Also fix episodes with no show_name at all
160+
echo "--- Checking for episodes with NULL show_name ---\n";
161+
$noShowName = Movie::where('title_type', 'TV Episode')
162+
->whereNull('show_name')
163+
->where(function ($q) {
164+
$q->whereNull('season_number')
165+
->orWhereNull('episode_number');
166+
})
167+
->get();
168+
169+
if ($noShowName->isNotEmpty()) {
170+
echo "Found {$noShowName->count()} episodes with no show_name:\n";
171+
foreach ($noShowName as $ep) {
172+
echo " ID={$ep->id} \"{$ep->primary_title}\" (IMDb: {$ep->imdb_id})\n";
173+
}
174+
} else {
175+
echo "No orphan episodes with NULL show_name remain.\n";
176+
}
177+
178+
echo "\n=== SUMMARY ===\n";
179+
echo "Total fixed: {$totalFixed}\n";
180+
echo "Total skipped: {$totalSkipped}\n";

0 commit comments

Comments
 (0)