/* ═══════════════════════════════════════════════════════════════ OUT-OF-CATALOG ARRIVALS (the /new/ surfaces only) OOC titles are auto-ingested and not yet hand-reviewed, so they stay out of fv_build_index() and off every browse page — that index is what /all/, the archives and the homepage rows all read, and every pick there is curator-vouched. "New on" is a different claim: it says a title landed on a service, not that we vouch for it. So the /new/ pages and the homepage's New shelf pull OOC arrivals from here and merge them in, and nothing else sees them. Entries are shaped like fv_build_index() entries so the callers need no special case beyond the merge. Cached for an hour; bump the key to force a rebuild. ═══════════════════════════════════════════════════════════════ */ function agmtw_ooc_arrivals( $geo = 'US' ) { static $memo = []; $map = [ 'US' => 'usa', 'CA' => 'canada', 'GB' => 'uk' ]; if ( ! isset( $map[ $geo ] ) ) $geo = 'US'; if ( isset( $memo[ $geo ] ) ) return $memo[ $geo ]; $key = 'agmtw_ooc_arrivals_v1_' . strtolower( $geo ); $cached = get_transient( $key ); if ( is_array( $cached ) ) return $memo[ $geo ] = $cached; global $wpdb; $prefix = $map[ $geo ]; $cutoff = time() - ( 90 * DAY_IN_SECONDS ); // 1) Arrival stamps first: only OOC titles that actually landed somewhere // recently are worth loading at all. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT m.post_id, m.meta_key, m.meta_value FROM {$wpdb->postmeta} m JOIN {$wpdb->posts} p ON p.ID = m.post_id WHERE p.post_status = 'outofcatalog' AND p.post_type IN ('post','shows') AND m.meta_key LIKE %s AND m.meta_value REGEXP '^[0-9]+$' AND CAST( m.meta_value AS UNSIGNED ) >= %d", $prefix . '-%-date', $cutoff ) ); if ( ! $rows ) { set_transient( $key, [], HOUR_IN_SECONDS ); return $memo[ $geo ] = []; } $plen = strlen( $prefix . '-' ); $slen = strlen( '-date' ); $arrivals = []; foreach ( $rows as $r ) { $streamer = substr( $r->meta_key, $plen, -$slen ); if ( $streamer === '' || $streamer === 'arrival' ) continue; $pid = (int) $r->post_id; $ts = (int) $r->meta_value; if ( ! isset( $arrivals[ $pid ][ $streamer ] ) || $arrivals[ $pid ][ $streamer ] < $ts ) { $arrivals[ $pid ][ $streamer ] = $ts; } } $ids = array_keys( $arrivals ); $ids_csv = implode( ',', array_map( 'intval', $ids ) ); // 2) Titles + type, 3) the taxonomies the /new/ table prints, 4) the // streamer tags an arrival has to be backed by. $meta_rows = $wpdb->get_results( "SELECT ID, post_type, post_title FROM {$wpdb->posts} WHERE ID IN ($ids_csv)" ); $tax_rows = $wpdb->get_results( " SELECT tr.object_id, tt.taxonomy, t.slug FROM {$wpdb->term_relationships} tr JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id JOIN {$wpdb->terms} t ON tt.term_id = t.term_id WHERE tr.object_id IN ($ids_csv) AND tt.taxonomy IN ('category','language','released','post_tag') " ); $sr_rows = $wpdb->get_results( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE post_id IN ($ids_csv) AND meta_key = 'staffrating'" ); $genres = $langs = $years = $streamers = []; foreach ( $tax_rows as $t ) { $pid = (int) $t->object_id; if ( $t->taxonomy === 'category' ) $genres[ $pid ][] = $t->slug; elseif ( $t->taxonomy === 'language' ) $langs[ $pid ][] = $t->slug; elseif ( $t->taxonomy === 'released' ) $years[ $pid ][] = $t->slug; elseif ( strpos( $t->slug, $prefix . '-' ) === 0 ) { $streamers[ $pid ][ substr( $t->slug, $plen ) ] = true; } } $sr = []; foreach ( $sr_rows as $r ) $sr[ (int) $r->post_id ] = (int) $r->meta_value; $out = []; foreach ( $meta_rows as $p ) { $pid = (int) $p->ID; // An arrival with no matching tag means the title has since dropped // off that service; same guard the main index applies. $arr = []; foreach ( ( $arrivals[ $pid ] ?? [] ) as $s => $ts ) { if ( isset( $streamers[ $pid ][ $s ] ) ) $arr[ $s ] = $ts; } if ( ! $arr ) continue; $out[] = [ 'id' => $pid, 'type' => $p->post_type, 'title' => html_entity_decode( $p->post_title, ENT_QUOTES, 'UTF-8' ), 'staffrating' => $sr[ $pid ] ?? 0, 'isPro' => false, 'isOOC' => true, 'streamers' => array_keys( $streamers[ $pid ] ?? [] ), 'genres' => $genres[ $pid ] ?? [], 'languages' => $langs[ $pid ] ?? [], 'released' => $years[ $pid ] ?? [], 'moods' => [], 'countries' => [], 'arrivals' => $arr, 'latestArrival' => max( $arr ), ]; } set_transient( $key, $out, HOUR_IN_SECONDS ); return $memo[ $geo ] = $out; }
Ghostlight (2024) | agoodmovietowatch
Back
Movie

Ghostlight 2024

Art heals in this illuminating film about loss, family, and hope

Our Take (by Renee Cuisia)

“The healing power of art” sounds cheesy, but it’s a statement made beautiful and true in Ghostlight. It’s the sensitively told and wonderfully performed story of an ordinary man who, up until this point, doesn’t even know how Romeo and Juliet ends. That’s how detached he is from art. But when Rita (Dolly de Leon) pushes him into the arena, he surprises himself and his family by being receptive to it. It’s the only way he can get in touch with his feelings, which is vital since he’s gone through an unspeakable loss recently. What that loss is isn’t immediately revealed in the film, but the small details snowball and eventually pummel you to the ground with its sheer tragedy. You’re either grinning or crying watching this, there’s no in-between. But if there were, you’d probably be marveling at the trio at the heart of this film: the family played by a real-life family. Keith Kupferer, who plays Dan, Tara Mallen, who plays Sharon, and Katherine Mallen Kupferer, who plays their daughter Daisy, are all compelling and dazzling in their own ways.

Notable Critics

"A film about the world-changing power of artistic communion, about how creativity, compassion, and forgiveness — of oneself and others — are all pit stops on the same human journey."

— Bilge Ebiri

"O’Sullivan and Thompson are aces at tucking themes, concepts, and ideas into their films that, in other directors’ hands, might feel a bit cheesy or chintzy. Instead, the duo handles them with the utmost respect and care."

— Kate Erbland

Synopsis

When a construction worker unexpectedly joins a local theater's production of Romeo and Juliet alongside his estranged teenage daughter, the drama onstage starts to mirror his own life.

More about it

What happens

When construction worker Dan (Keith Kupferer) is pulled in to play Romeo in a community theater play, he slowly learns to accept a recent tragedy that’s shaken his entire family.

What sets it apart

There are plenty I’d like to mention, but I’m putting my money on Mallen-Kupferer. It’s her intense performance that’ll be remembered long after the years have gone.

TL;DR

You’ll either be grinning from ear to ear or wiping away tears watching this, there’s no in between.

Awards

Spirit Awards

2 nominations

Nominated: Best Lead PerformanceNominated: John Cassavetes Award

Sundance

1 nomination

Nominated: Official Selection

Nat. Board of Review

1 win

Won: Top 10 Independent Films

Comments

Add your review

Your email address will not be published.*

About the author

Renee Cuisia

Renee Cuisia

Renee Cuisia is the lead curator at A Good Movie to Watch. In her spare time, she likes to watch K-dramas and analyze them to death. She's also seen You've Got Mail one too many times but is still convinced it's one of the greatest films out there.