Directory Theme class reference
Video Theme — Class reference
The 44 classes that make _vt behave differently from the shared Directory core. Generated from the theme source.
← Back to Video Theme features
Actions Actions.php
Video member hub — the WRITE side. The handful of things the rebuilt members' area lets a member DO from the page itself, without opening the editor: change who can see a video, delete one, tidy their own library (watch later, liked, history, subscriptions), and buy — or stop — a paid featured placement with credits (Hub\Promote owns that mechanism; the two endpoints here are only its doorway). Every endpoint is wp_ajax_ only — never wp_ajax_nopriv_, because there is nothing here a signed-out visitor could legitimately do — and every one of them checks, in this order: the shared ppt_vh nonce, that the member is signed in, and that the thing being changed is theirs (ListingEditor::canEdit for a listing, the current user id for anything in their own library). Library writes go through the class that owns that store; nothing here invents storage of its own. Replies are always wp_send_json_success / wp_send_json_error carrying a message written in plain English, so the JS can show the server's words rather than inventing its own. Successful writes also return the figure that changed (the new status row, the new list count), so the page can update without a reload. Deleting TRASHES. A member clicking "Delete" on their own upload is not asking for an unrecoverable action, and the site owner can still restore it from the bin.
Channel Channel.php
Video — who a video is PUBLISHED BY on the public side. There is no channel post type on this fork: a channel IS the WordPress user who uploaded the video. One reader so the watch page, the result cards and the Shorts feed cannot drift about the name they print.
Checkout Checkout.php
Video Theme — single-video pay-per-view checkout (Ppt_vt). A viewer clicks "Unlock" on a paid video; that routes here as /checkout/?unlock=
_ph, _ct, _sp): it reuses the same ppt_gateway_start / ppt_gateway_verify contracts, the shared ppt_orders CPT (Ppt\Admin\Orders), the /checkout/ + /callback/ virtual pages and the receipt email templates, without touching CheckoutFlow. Order refs are prefixed PPV- so Pages::content() knows to route callbacks back here. Simpler than the photo checkout in two ways, both deliberate. There are no tiers — a video has ONE price and one outcome — and there is no credits rail, because the credits capability is off for the video profile (see ThemeProfiles::featuresMatrix) and a payment path the profile does not ship has no business appearing at checkout. Both would slot in the same way _ph does if that ever changes.
Data Data.php
Video member hub — the READ side. One place the rebuilt members' area asks for everything it draws: the member's own videos and how they are doing, their channel header, a 28-day analytics block, the comments waiting for a reply, their library (watch later / history / liked / subscriptions / playlists) and their credit wallet. The view layer (inc/_vt/Hub/HubUi.php + inc/_vt/views/account.php) renders these arrays and nothing else — no queries in the templates. Nothing here has its own storage. Every figure is read back out of the store that already owns it: views → the ppt_listing_views table (Ppt_vt\ListingViews) likes → ppt_like_count post meta (Ppt_vt\ListingActions) comments → wp_comments watch later / liked → user meta (Ppt_vt\ListingActions) history → user meta (Ppt_vt\History::watchedIds) subs → the follow graph (Ppt\Account\Follows) sub DATES → Ppt_vt\Hub\FollowLog (started recording recently; see below) playlists → the listing_playlist taxonomy (Ppt_vt\Playlists) wallet → Ppt\Credits\Wallet + Config promotion → Ppt_vt\Hub\Promote (the paid featured placement and its meta) TWO THINGS ARE NOT KNOWABLE and are reported as such rather than guessed: 1. Historic likes and comments. Both stores hold a CURRENT total with no dated trail, so "likes in the 28 days before last" cannot be derived. analytics() returns the same number for the previous period and flags it likesPrevKnown/commentsPrevKnown = false, so the view omits the change line instead of printing a fabricated 0% . 2. Subscriber history before FollowLog existed. Gains are counted only from the day recording began; subsSince carries that date so the hub can say so. Costs: one query per concern, never one per video. Views, likes and comment counts for the whole table are each fetched in a single batched statement; playlist terms come through one wp_get_object_terms() call.
DemoAccounts DemoAccounts.php
Video Theme — what the DEMO member walks into (Account\DemoLogin's plain test member). The video hub is a channel page: My Videos, Analytics, Library, Wallet. A member with nothing in any of them lands on a first-run welcome, which is not what a one-click demo login is for. So the member is furnished with: a channel four showroom videos lent to them (DemoLogin lends them — the count is raised here from none; a fresh site creates the rows from the niche's sample cards, and the fork's DemoProfiles stamps channel / duration / format on each); an audience three subscribers (throwaway members carrying DemoLogin::META_FLAG, so the go-live purge removes them with everything else) who follow the channel and have left a comment each; numbers views spread over the last 28 days on every lent video, so Analytics has a series and My Videos has a count per upload; a library two videos in Watch later, two liked, three in History — drawn from the site's other showroom videos when there are any, else from the member's own uploads. Everything is undone at purge: the subscribers are demo-flagged users, the library lives in the member's own meta, the comments are deleted here, and the view rows are removed for the lent listings so the rows go back to the pool clean.
DemoProfiles DemoProfiles.php
Video — the demo CHANNEL a seeded listing was published by. Every other fork's seeded demo listing can get away with belonging to the site administrator, because a directory listing is a business and a shop listing is a product. A video is published BY somebody, and the video cards print that name under every thumbnail: on a fresh install of the three creator designs all nineteen cards read "Mark", while the design's own dressing promised Velvet Hour, Maison Noir and Scarlet Rooms. Same complaint the showroom had, one layer down — see {@see Channel::DEMO_META}, which this fills in for REAL seeded posts. Reached through the generic fork hook: Tools\SampleData::seedProfileMeta() calls \Ppt`
Expiry Expiry.php
PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.
Explore Explore.php
Explore (Shorts feed) for the Video Theme (_vt). The shell's left-nav "Explore" opens a full-screen, one-at-a-time Shorts viewer at /explore/ — a vertical 9:16 player with prev/next navigation, cycling ONLY through short-format videos (VideoDetails::SHORT), newest first, excluding Unlisted ones. The whole list is embedded as JSON and the JS swaps the player client-side, so navigation is instant (no reload). Wrapped in get_header()/get_footer() so it adopts the active video design's sidebar shell automatically. Registered in Theme.php's _vt service list, so it only exists on the video theme.
FollowLog FollowLog.php
Video — WHEN a channel gained each subscriber. In the video fork a follow IS a subscription ({@see \Ppt\Account\Follows}), and that graph stores only the current edge — who follows whom, right now. It carries no dates, so "subscribers gained in the last 28 days" could not be answered from it at all, and the number of subscribers a channel had a month ago is simply not recorded anywhere. This is the smallest thing that fixes that going forward: a capped list of [timestamp, follower id] pairs on the FOLLOWED user, appended whenever the existing ppt_seller_follow_changed action fires with a gain. Nothing is back-filled — a channel with 4,000 historic followers starts this log empty, and the hub says so rather than inventing a curve. {@see startedOn()} is what lets the member area print "counted from …" beside the figure. Losses are deliberately not logged. A "subscribers gained" number is what this feeds, the live total is always Follows::count(), and keeping only gains halves what the capped list has to hold.
Gallery Gallery.php
Single-listing gallery styles. On the video theme only "Standard" is offered (see styles() below); the layout is stored in the ppt_design option via Branding and rendered by the single-listing template for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default]
Geocoder Geocoder.php
Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.
History History.php
Watch history page for the Video Theme (_vt). The shell's left-nav "History" (under Your library) opens /history/ — the videos the logged-in viewer has recently watched, most-recent first. History keeps its OWN per-user list in user meta, written on every watch page, exactly like the Watch later and Liked videos lists beside it (Ppt_vt\ListingActions). It deliberately does NOT read the ppt_listing_views analytics log any more. That log asks the ppt_record_listing_view filter first, and Ppt\Frontend\ViewExclusions ("don't count my own visits", both rules ON by default) drops the row whenever the viewer runs the site or owns the video — so an owner's history could never fill. The two features want opposite things: analytics wants your own visits OUT, a watch history wants them IN. Analytics also throttles to one row per listing per hour, which would stop a re-watch from moving a video back to the top. The old log is still read once, as a fallback, so history recorded by an earlier build isn't lost. The page manages itself: a ✕ on each card removes that one video, and "Clear all watch history" empties the list, both over AJAX (signed-in only, acting on the caller's own meta — no capability is involved, so this needs no MemberCaps allow-list entry). In a design preview / demo mode (no real watch log) it falls back to a few curated sample videos so the showroom page reads on-brand. Wrapped in get_header()/get_footer() so it adopts the active video design's sidebar shell. Registered in Theme.php's _vt service list, so it only exists on the video theme.
HoverPreview HoverPreview.php
Video theme (_vt) — hover-to-preview on listing cards, site-wide. Every _vt block paints its thumbnail as a CSS background-image on its own element (ppt-vlg-thumb, ppt-vll-clip, …), and there are ~100 of them. Adding a <video> to each block class would be a hundred edits and a hundred chances to drift, so this does it in one place instead: Blocks\Support\LiveItems is the single thing that fills those cards with real listings on a live site, and its ppt_live_items_row filter hands over the post id for every row it builds. We record the clip for each id there, print the map once in the footer, and one small script attaches the behaviour to any card whose link matches. A block added tomorrow gets this for free. The playback mechanics are the ones proved on cue_hero (see the hover-video pattern): data-src rather than src so nothing is fetched until first hover, pointer-events: none so the card stays clickable through the clip, fade in on the playing event rather than on mouseenter so the poster never blinks to black, and hard bail-outs for touch pointers and prefers-reduced-motion.
HubUi HubUi.php
Video theme (_vt) members' area — the app's assets and its configuration. The screens are drawn in the browser by assets/js/vt-hub.js, which is a PORT of the approved mockup (dt10-backups/mockups/video-member-area/a-channel.html, published as "Vidora Channel Hub"): same markup, same class names, same CSS (assets/css/vt-hub.css). This class is the seam between that port and the site — it hands the script the member's REAL data, as window.pptVhCfg, and nothing else. Read side lives in Ppt_vt\Hub\Data; the write side (visibility, delete, library removals) is Ppt_vt\Hub\Actions. The Account and Messages tabs are NOT drawn here: they are the shared hub panels (inc/Account/views/parts/hub-panels.php), shown in place of this app.
LikedVideos LikedVideos.php
Liked videos page for the Video Theme (_vt). The shell's left-nav "Liked videos" (under Your library) opens /liked-videos/ — the grid of every video the logged-in viewer has liked. Likes come from the existing Ppt_vt\ListingActions like store (user meta ppt_likes), the same toggle used by the heart button on the single-listing watch page and in the Shorts rail: like a video there and it appears here; un-like it and it drops off. Newest-liked first. Wrapped in get_header()/get_footer() so it adopts the active video design's sidebar shell automatically. Registered in Theme.php's _vt service list, so it only exists on the video theme.
ListingActions ListingActions.php
Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.
ListingCard ListingCard.php
The listing card for the Video Theme (Ppt_vt). A YouTube-style video card: a 16:9 thumbnail with a hover play button and a duration (or LIVE) badge, then a channel avatar beside a two-line title, the channel name and a "views · age" meta line — the look of a video platform's grid. Forked from Ppt\Directory\ListingCard for the video profile so the video grid can diverge from the generic directory card. Every _vt surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a video looks the same everywhere. Its extra classes (.ppt-lc--video, .ppt-lc-vd-*) live in assets/css/listing-card.css. Normalized shape (adds channel/meta/duration/live to the base shape): name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id, channel, meta, duration, live (bool) meta is the ready-made "84K views · 3 days ago" line; a real post builds it from its view count + publish date, a demo row supplies it directly.
ListingEditor ListingEditor.php
Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.
ListingFaq ListingFaq.php
Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.
ListingHours ListingHours.php
Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.
ListingLocation ListingLocation.php
Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().
ListingMap ListingMap.php
The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.
ListingPricing ListingPricing.php
Single-listing pricing plans. A listing owner enters one or more plans / packages on the submission form (a name, a price, and a short description); they're stored in the pricing_plans post meta as a list of ['name' => …, 'price' => float|null, 'desc' => …]. The LOWEST priced plan is mirrored into the searchable price meta (Ppt_vt\SearchPage::PRICE_META) by the editor on save, so the price range filter, the price sort, and the result-card price all work — that's the link between "enter your pricing" and "find listings by price". Prices are stored in the site's base currency; render() converts + formats via Ppt\Content\Currencies for the viewer's chosen currency.
ListingSections ListingSections.php
PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.
ListingViews ListingViews.php
PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).
Live Live.php
Go Live — the browser-broadcasting service for the Video Theme (_vt). Ties the member "studio" (inc/_vt/views/editor-live.php + assets/js/go-live.js) and the watch-page live player (assets/js/live-watch.js) to a streaming Provider (Ppt\Stream\Cloudflare). The flow: 1. A member creates a LIVE-format listing (VideoDetails::LIVE) from the hub and lands in the studio. 2. "Go Live" → ACTION_START mints a per-listing live input on the provider (once, then reused), publishes the listing, flips its state to LIVE_ON, and returns the per-input WHIP ingest URL so the browser can publish its camera over WebRTC. 3. Viewers open the watch page; ACTION_STATUS (public, transient-cached) reports whether a broadcast is connected and hands back the HLS manifest — the live stream while on air, the auto-generated recording once it ends. 4. "End" → ACTION_STOP flips the state to LIVE_ENDED; the recording becomes the archive on the same page. SECURITY: the provider API token never leaves the server. ACTION_START returns only the per-input WHIP capability URL + the public HLS URL. Start/stop require the member to own the listing; status is a read-only public reconcile with a short cache.
LiveNow LiveNow.php
Live Now directory for the Video Theme (_vt). The shell's left-nav "Live now" opens /live/ — a grid of every channel that is broadcasting right now (LIVE-format listings whose state is LIVE_ON, Unlisted excluded), most-recently-started first. Each card links straight to that stream's watch page. Wrapped in get_header()/get_footer() so it adopts the active video design's sidebar shell automatically. "Currently live" reads the stored broadcast state (VideoDetails::LIVE_STATE_META), the same source the listing card's LIVE badge trusts — kept fresh by Live::ajaxStart/ Stop and reconciled against the provider whenever a viewer opens a watch page. Registered in Theme.php's _vt service list, so it only exists on the video theme.
Media Media.php
Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.
Playlists Playlists.php
Playlists for the Video Theme (_vt). A creator groups their videos into named playlists. Each playlist is a term in the listing_playlist taxonomy, OWNED by a user (term meta self::OWNER_META); a video (listing) is added to a playlist with wp_set_object_terms. The upload/edit form's right column lets the owner create a playlist and tick which ones this video belongs to (editorCard + saveFromForm); a new one is created inline over AJAX (ajaxNew). Deliberately thin, mirroring _cd\VehicleSpecs: register the taxonomy on init, expose value helpers + an editor card + a save method. Registered in Theme.php's _vt service list so it only loads when the video fork is active.
Ppv Ppv.php
Pay-per-view for the Video Theme (_vt) — the one place that answers "is this video locked, for whom, and at what price". The video designs (Velour's premiere masthead, Siren's film masthead, both of their card grids) have always DRAWN pay-per-view: a blurred still behind a lock ring, a gold price pill, an "Unlock full episode" button. Until now that was editorial copy on the block and nothing else — a live site could not sell a single video, and the lock disappeared the moment real content replaced the demo rows. This is the feature those designs were drawing. THE MODEL. An owner turns pay-per-view on for one video and names a price, in the site's base currency, plus how many seconds of it anybody may watch for free. A viewer who has not paid gets that free window and an unlock button; a viewer who has paid gets the whole thing, permanently (Unlocks is a ledger of purchases, not a rental clock). Payment runs through the fork's own checkout (_vt\Checkout), which is the shared gateway/orders machinery with a PPV- order prefix — the same shape the stock-photo fork uses for a licence. WHO ALWAYS WATCHES FREE, and why the list is exactly this: · the video's own author — they uploaded it and must be able to check their page; · anyone who can edit it (admins/editors), who can reach the file in the media library anyway, so gating them here would be theatre rather than security; · a member whose plan includes members-only media. That is deliberately the SAME capability (locked_media) the photo/clip lock uses, via Membership::unlocksMedia — "members get every upload; single films can be unlocked on their own if you would rather not subscribe" is the promise the demo profile copy already makes, and one capability keeps that promise true in both places at once. WHAT THIS CLASS IS NOT. It decides ENTITLEMENT; it does not deliver bytes. The free preview window is enforced in the player, which is a courtesy limit, not a wall — the file itself is closed off by Ppt_vt\Stream, which serves a locked video only through a signed, expiring URL and shuts the REST and attachment-permalink doors that would otherwise hand out the original. Ask this class "may they?"; ask Stream "how does the byte stream reach them?".
Promote Promote.php
Promote — a member pays credits to sit at the top of video search for a set number of days. WHAT IS BOUGHT. Not a badge alone and not a permanent rank: for 7, 14 or 30 days the video is lifted above everything else in the DEFAULT order of the video search / archive, and it carries the site's existing "Featured" mark while it runs. The member pays out of the shared credit wallet (Ppt\Credits\Wallet), so the wallet is the product and this class is only the mechanism. IT REUSES THE featured FLAG RATHER THAN INVENTING A SECOND ONE. Every surface that already knows what a featured listing is — the card ribbon, the "Featured first" sort, the search page's Featured rail, the admin star, the mobile app's featured feed — is fed by one meta key (featured = '1', Ppt_vt\SearchPage::FEATURED_META). A paid promotion MIRRORS that flag on for its window and takes it off again at the end, so every one of those surfaces honours a promotion without a line of change. The admin is the other writer of that same flag, and the two must not tread on each other: ppt_featured_admin remembers that the admin had already ticked the star BEFORE the promotion started, and expiry then leaves it alone instead of quietly un-featuring a video the site owner chose to feature. THE TIMER IS THE TRUTH. ppt_featured_until (unix) is what every read compares against, so an expired promotion can never be shown as running, whatever the cron did or did not do. The daily ppt_vt_promote_expire sweep exists to TIDY — to take the featured flag back off — not to decide when a promotion ended. A lazy sweep of the member's own rows runs whenever the hub reads their promotion, so the tidying is never more than one page-load late for the person it concerns. STOPPING EARLY REFUNDS WHOLE UNUSED DAYS; EXPIRING REFUNDS NOTHING. floor(credits × daysLeft ÷ daysTotal) — part-days are not returned, because they were used. A promotion that simply ran its course was delivered in full and there is nothing to give back. ONE AT A TIME, PER MEMBER. A second promotion while one is running is refused rather than queued or stacked: two windows the member cannot see separately is money spent on something nobody can point at.
Reviews Reviews.php
Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.
SearchPage SearchPage.php
Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | popular | newest | oldest | rating | price_low | price_high | title (popular = Trending: most views in the trailing 7 days) paged pagination
SingleListing SingleListing.php
Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.
Stream Stream.php
Delivery for pay-per-view videos (_vt) — how the bytes reach a viewer, and how they stop reaching one who has not paid. Ppv answers "may they watch?". This answers "then how do they get the file?", and it exists because an entitlement check on the watch page is worth nothing while the raw wp-content/uploads/…mp4 URL is sitting in the page source. A paid video is never printed as its own attachment URL: it is served only through a short-lived, HMAC-signed link bound to the viewer, and the other doors WordPress opens onto the same file are shut — exactly the shape Ppt\Account\LockedMedia uses for members-only photos, and Ppt_ph\Download for a licensed original. THE DOORS THIS CLOSES, all on the one fact that a video's parent listing is sold per view: 1. GET /wp-json/wp/v2/media?parent=source_url of every paid video on the site, enumerable by listing id. 2. The attachment permalink (/video///, ?attachment_id=N), which serves — or, on a modern install, redirects straight to — the original. RANGE REQUESTS ARE THE POINT. A <video> element does not download a file, it seeks around inside one, so this speaks HTTP 206 properly. Serving the whole body for every request (which a plain readfile() does) breaks the scrub bar, and Safari will not play a source that ignores Range at all. WHAT THIS DOES NOT DO — stated plainly, because the gap is deliberate and known: · The free preview window is enforced in the PLAYER, not in this stream. An unentitled viewer is handed a signed link to the real file and the player stops at the owner's preview mark. That defeats casual watching, not a determined viewer with developer tools. Cutting the file server-side is not safe to do blind — an MP4 whose moov atom sits at the end will not play at all from a byte prefix, and a VBR file's bytes do not map to seconds — so the honest fix is a transcoded preview rendition, which needs ffmpeg and belongs in its own step. WHAT USED TO BE HERE, and no longer is: this header carried a second caveat saying the original still sat at a public uploads path, so closing these doors removed the way to LOOK a file UP without making a correctly guessed path 404. Ppt_vt\Vault closes that: a video that goes on sale has its bytes MOVED into Ppt\Media\PrivateStore, and Vault::pathFor() is what this class reads them back through. There is no longer a public path to guess. The one case where the old caveat still applies is a host where the vault could not be written at all — Vault fails open on purpose, so the video keeps playing from its public path and this endpoint keeps gating who may WATCH it. Weaker, and deliberately not broken; PrivateStore::isReachable() is what tells an admin they are in that case.
Subscriptions Subscriptions.php
Subscriptions page for the Video Theme (_vt). The shell's left-nav "Subscriptions" opens /subscriptions/ — a list of every channel (member) the logged-in viewer follows, using the existing Ppt\Account\Follows graph (there's no separate "subscribe" store; in the video theme a follow IS a subscription). Each channel shows its avatar, name, subscriber count and video count, and links to the channel's public author profile. Guests get a sign-in prompt. Wrapped in get_header()/get_footer() so it adopts the active video design's sidebar shell automatically. Registered in Theme.php's _vt service list, so it only exists on the video theme.
TermMeta TermMeta.php
Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.
Unlocks Unlocks.php
Purchased pay-per-view videos for the Video Theme (_vt) — the entitlement ledger. When a viewer completes an unlock (Ppt_vt\Checkout), the video they bought is recorded here as a row in their own user meta. It answers the two questions the rest of the theme asks: "has this viewer unlocked this video?" (so the wall lifts and the price pill becomes a Watch button) and "what have they unlocked?" (the members' area list). A sibling of Ppt_ph\Licenses, deliberately simpler: a stock photo is bought under a licence TIER, so that ledger carries the tier and its legal type. A video has one price and one outcome — you can watch it — so a row is just the video, the order it came from, and what was paid. Unlocks are PERMANENT: there is no expiry column, because nothing here rents. Ppv owns the entitlement QUESTION (it also lets the author, an editor and a paid-up member through without a purchase). This class only knows about purchases, so everything else should ask Ppv::canWatch(), never this directly.
Uploads Uploads.php
Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.
Vault Vault.php
The vault — where a pay-per-view video's actual bytes live (_vt). Ppv decides who may watch. Stream decides how the bytes travel. This decides WHERE they sit while they wait, and it exists because the other two were built on a floor that was never solid: a WordPress upload lives at a public, guessable URL, and no amount of entitlement checking at the front door matters while the back door is a path anybody can type. Stream's own header said as much — "closing these doors removes the way to LOOK IT UP; it does not make a correctly guessed path 404". This is what makes it 404. WHAT HAPPENS. When an owner puts a price on a video, its file is MOVED out of wp-content/uploads/… into Ppt\Media\PrivateStore — outside the media library, in a denied directory, under a 32-hex random name, reachable only through code. The attachment row stays exactly where it was, so the title, the poster, the duration and every query that finds the video go on working; only the bytes move. Take the price off again and they move back. WHY THE ATTACHMENT KEEPS ITS _wp_attached_file. It is deliberately NOT rewritten to point at the vault. Two reasons, and both are the same reason: anything that resolves a file the ordinary WordPress way — get_attached_file(), wp_get_attachment_url(), a plugin, a sitemap, an image CDN — now resolves to a path that does not exist, which is precisely the failure we want, instead of quietly serving the paid file from a new location. And it records where the file must go back to when the price comes off. FAIL OPEN ON STORAGE, NEVER ON ENTITLEMENT. If the private directory cannot be created or the move fails, vault() gives up and says so. The video stays public and keeps playing, the owner keeps their price, and the wall still goes up for anybody who has not paid — the gate is weaker than it should be, exactly as weak as it was before this class existed, and nothing is broken. A storage problem must never cost somebody the video they bought.
VideoDetails VideoDetails.php
Video format / duration / visibility for the Video Theme (_vt). The video fork stores its clips as WP attachments parented to the listing (see Uploads / Media). This service adds the small amount of video-specific metadata that the YouTube-style upload wizard, watch page and cards need on top of that: - FORMAT ('regular' | 'short') — chosen up front in the "Create" menu (Upload video vs Create Short) and stamped on the draft at creation. Drives the vertical 9:16 player + card variant for Shorts vs the 16:9 layout for regular videos. - DURATION (seconds) — captured client-side from the uploaded file's video.duration (no server ffmpeg) and posted with the form, so real cards can finally show a running-time badge (previously demo rows only; see ListingCard::fromSample). - UNLISTED (flag) — the "Unlisted" visibility option: the video keeps a public permalink but is dropped from search / archive / related feeds. "Private" is a real WP private status; "Public" is publish (or pending under moderation). Visibility itself is applied in ListingEditor::save(); this class owns the flag helpers + the query clause that hides unlisted videos. Deliberately thin, mirroring _cd\VehicleSpecs / _ll\CourseDetails: the wizard and admin form call saveFromForm($pid, $in); templates read format()/isShort()/ durationBadge(); SearchPage merges unlistedExclusionClause() into its meta query.
VideoLink VideoLink.php
Video Theme — a listing whose video lives on YouTube or Vimeo rather than in the media library. The listing is still an ordinary listing: same editor, same watch page, same cards, same search. Only the SOURCE differs, and only three things read that difference — the editor (which source is being filled in), the watch page (an <iframe> instead of a <video>), and the save path (which source wins). Everything else keeps working because of one decision: on save we sideload the provider's own thumbnail as the listing's FEATURED IMAGE. Cards, search results, the watch-page poster, Open Graph tags and the member's hub all already read the featured image, so none of them needed a line changing — and the member can still replace it from the editor's Thumbnail tab afterwards, exactly as with an upload. Deliberately NOT supported here, because a link is somebody else's file: - Ppt_vt\Vault (taking the bytes out of the uploads tree) — there are no bytes. - Ppt_vt\Stream signed playback — nothing to sign. - Pay-per-view TIMED PREVIEWS — we cannot stop somebody else's player at 30s and mean it. A locked paid video with a link shows the unlock wall and no preview, and the editor says so. Selling a linked video is still allowed (the owner's call); see previewApplies(). One source per listing: the video profile is singleMedia, so a listing has an upload or a link, never both. The editor says which it means by WHICH BOX IS FILLED — the source sits in the media tab strip (Video | Thumbnail | YouTube | Vimeo) with a field per provider, so there is no separate chooser and no hidden state for the JavaScript to keep in step.
WatchGate WatchGate.php
"Sign in to watch" — the site-wide rule that puts playback behind an account. One switch (Settings ▸ Listings ▸ Submissions ▸ Sign in to watch) and one question: is the person looking at this page signed in? When they are not, every surface that would otherwise PLAY a video draws a sign-in panel in the player's place instead — the watch page (ordinary uploads and live streams alike) and the Shorts feed, which autoplays a full clip with no click at all and would otherwise be the way round it. The page itself is deliberately NOT redirected. Title, description, channel and the Up next rail still render, so the video keeps its place in Google and a shared link still previews — the thing that requires an account is WATCHING, and that is the thing the panel withholds. Clicking the panel (anywhere on the stage) goes to the site's sign-in screen and comes back to the same video afterwards. WHAT THIS IS NOT. This closes the ways in that the site itself offers; the file behind a free video still sits at an ordinary uploads URL, as every WordPress upload does, so someone who already has that URL can still fetch it. Making that impossible means keeping the originals out of the public tree and serving them signed — which is exactly what Ppt_vt\Vault + Ppt_vt\Stream already do for PAID videos, and a deliberate, separate step for the rest of a library. Said plainly rather than implied: this is a members' door, not an anti-piracy wall.
WatchLater WatchLater.php
Watch later page for the Video Theme (_vt). The shell's left-nav "Watch later" (under Your library) opens /watch-later/ — the queue of videos the logged-in viewer has saved for later, newest-saved first. The queue is a per-user list in user meta (Ppt_vt\ListingActions::WATCH_META), toggled by the "Watch later" button on the single-listing watch page: save a video there and it appears here; remove it and it drops off. In a design preview / demo mode (no real queue) it falls back to a few curated sample videos so the showroom page reads on-brand. Wrapped in get_header()/get_footer() so it adopts the active video design's sidebar shell. Registered in Theme.php's _vt service list, so it only exists on the video theme.