PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.3.2
Presto Player v4.3.2
4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / hooks / useMediaDetail.js
presto-player / src / admin / dashboard / hooks Last commit date
test 1 month ago useCompleteOnboarding.js 1 month ago useDateRangePicker.js 2 months ago useEmail.ts 2 months ago useEngagementChartData.js 2 months ago useLicenseSettings.js 2 months ago useLink.js 2 months ago useMediaDetail.js 2 months ago useMediaLibrary.js 2 months ago useMediaList.ts 1 month ago usePerformanceSettings.js 2 months ago useRegisterActivePage.js 2 months ago useSettingOption.js 2 months ago useSimpleSettingsPage.js 2 months ago useTopPerforming.js 2 months ago useTopVideosPaginated.js 2 months ago useUpgradeCTA.js 2 months ago useUserDetail.js 2 months ago
useMediaDetail.js
204 lines
1 import { useState, useEffect, useCallback, useRef } from "@wordpress/element";
2 import apiFetch from "@wordpress/api-fetch";
3 import {
4 formatHMS,
5 getLastNDays,
6 toAnalyticsDate,
7 DEFAULT_ANALYTICS_DAYS,
8 } from "../utils";
9
10 /**
11 * Hook to fetch analytics data for a single media item.
12 *
13 * Fetches:
14 * - Media metadata (title, poster, shortcode) via the custom Presto Player API
15 * - Optionally enriches with WP REST API data if post_id is available
16 * - Unique views for the media item
17 * - Average watch time for the media item
18 * - Audience retention data (watch-time distribution over video duration)
19 *
20 * All analytics endpoints accept optional start/end date query params.
21 */
22 const useMediaDetail = (mediaId) => {
23 const [media, setMedia] = useState(null);
24 const [stats, setStats] = useState({
25 uniqueViews: 0,
26 avgWatchTime: "00:00:00",
27 retentionData: [],
28 });
29 const [isLoading, setIsLoading] = useState(true);
30 const [error, setError] = useState(null);
31 const abortControllerRef = useRef(null);
32
33 const fetchDetail = useCallback(
34 async (startDate, endDate) => {
35 if (!mediaId) return;
36
37 if (abortControllerRef.current) {
38 abortControllerRef.current.abort();
39 }
40 abortControllerRef.current = new AbortController();
41
42 try {
43 setIsLoading(true);
44 setError(null);
45
46 // Default to last 7 days when no range was supplied so the
47 // detail page mirrors the Analytics page's default scope.
48 const range =
49 startDate && endDate
50 ? { from: startDate, to: endDate }
51 : getLastNDays(DEFAULT_ANALYTICS_DAYS);
52 const dateQueryArgs = {
53 start: toAnalyticsDate(range.from),
54 end: toAnalyticsDate(range.to),
55 };
56
57 // Fetch video metadata from custom Presto Player API (uses custom table ID)
58 const videoResponse = await apiFetch({
59 path: `/presto-player/v1/videos/${mediaId}`,
60 signal: abortControllerRef.current.signal,
61 });
62
63 let title = videoResponse.title || `Media #${mediaId}`;
64 let poster = null;
65 let src = videoResponse.src || "";
66 let provider = videoResponse.type || "";
67 let preset = {};
68 let branding = {};
69 let blockAttributes = {};
70 let tracks = [];
71 const postId = videoResponse.post_id;
72
73 // Use the list endpoint with `include` instead of fetching the post
74 // by ID directly: when the post no longer exists (orphaned video row),
75 // this returns 200 OK with an empty array instead of logging a 404 to
76 // the network panel on every detail-page load.
77 if (postId) {
78 try {
79 const wpResponse = await apiFetch({
80 path: `/wp/v2/presto-videos?include=${postId}&_embed=1`,
81 signal: abortControllerRef.current.signal,
82 });
83 const wpPost = Array.isArray(wpResponse) ? wpResponse[0] : null;
84
85 if (wpPost) {
86 if (wpPost.post_title) {
87 title = wpPost.post_title;
88 } else if (typeof wpPost.title === "string") {
89 title = wpPost.title;
90 } else if (wpPost.title?.rendered) {
91 title = wpPost.title.rendered;
92 }
93
94 poster =
95 wpPost.details?.poster ||
96 wpPost._embedded?.["wp:featuredmedia"]?.[0]?.source_url ||
97 null;
98 provider = wpPost.details?.provider || provider;
99 preset = wpPost.details?.preset || {};
100 branding = wpPost.details?.branding || {};
101 blockAttributes = wpPost.details?.blockAttributes || {};
102 tracks = wpPost.details?.tracks || [];
103 }
104 } catch (wpErr) {
105 if (wpErr.name !== "AbortError") {
106 console.warn(
107 "WP post data unavailable, using basic video data:",
108 wpErr
109 );
110 }
111 }
112 }
113
114 setMedia({
115 id: videoResponse.id,
116 title,
117 poster,
118 src,
119 provider,
120 preset,
121 branding,
122 blockAttributes,
123 tracks,
124 shortcode: postId
125 ? `[presto_player id=${postId}]`
126 : `[presto_player id=${videoResponse.id}]`,
127 });
128
129 // Fetch all analytics data in parallel for this specific media
130 try {
131 const [viewsRes, avgWatchTimeRes, timelineRes] = await Promise.all([
132 apiFetch({
133 path: wp.url.addQueryArgs(
134 `/presto-player/v1/analytics/video/${mediaId}/views`,
135 dateQueryArgs
136 ),
137 signal: abortControllerRef.current.signal,
138 }),
139 apiFetch({
140 path: wp.url.addQueryArgs(
141 `/presto-player/v1/analytics/video/${mediaId}/average-watchtime`,
142 dateQueryArgs
143 ),
144 signal: abortControllerRef.current.signal,
145 }),
146 apiFetch({
147 path: wp.url.addQueryArgs(
148 `/presto-player/v1/analytics/video/${mediaId}/timeline`,
149 dateQueryArgs
150 ),
151 signal: abortControllerRef.current.signal,
152 }),
153 ]);
154
155 const viewCount = parseInt(viewsRes) || 0;
156 const avgFormatted = formatHMS(parseFloat(avgWatchTimeRes) || 0);
157
158 const retentionData = Array.isArray(timelineRes) ? timelineRes : [];
159
160 setStats({
161 uniqueViews: viewCount,
162 avgWatchTime: avgFormatted,
163 retentionData,
164 });
165 } catch (analyticsErr) {
166 // Analytics may not be available (free version), use mock fallback
167 if (analyticsErr.name !== "AbortError") {
168 console.warn(
169 "Analytics data unavailable, using defaults:",
170 analyticsErr
171 );
172 setStats({
173 uniqueViews: 0,
174 avgWatchTime: "00:00:00",
175 retentionData: [],
176 });
177 }
178 }
179 } catch (err) {
180 if (err.name !== "AbortError") {
181 setError(err.message || "Failed to load media details");
182 console.error("Error fetching media detail:", err);
183 }
184 } finally {
185 setIsLoading(false);
186 }
187 },
188 [mediaId]
189 );
190
191 useEffect(() => {
192 fetchDetail();
193 return () => {
194 if (abortControllerRef.current) {
195 abortControllerRef.current.abort();
196 }
197 };
198 }, [fetchDetail]);
199
200 return { media, stats, isLoading, error, refetch: fetchDetail };
201 };
202
203 export default useMediaDetail;
204