tangled
alpha
login
or
join now
dunkirk.sh
/
pstream-ng
1
fork
atom
pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
1
fork
atom
overview
issues
pulls
pipelines
Add split overlay scraping part with title and info
Pas
3 weeks ago
b3c2d58f
24132618
+184
-52
1 changed file
expand all
collapse all
unified
split
src
pages
parts
player
ScrapingPart.tsx
+184
-52
src/pages/parts/player/ScrapingPart.tsx
···
9
9
scrapePartsToProviderMetric,
10
10
useReportProviders,
11
11
} from "@/backend/helpers/report";
12
12
+
import { getMediaDetails, getMediaLogo } from "@/backend/metadata/tmdb";
13
13
+
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
12
14
import { Button } from "@/components/buttons/Button";
13
15
import { Loading } from "@/components/layout/Loading";
14
16
import {
15
17
ScrapeCard,
16
18
ScrapeItem,
17
19
} from "@/components/player/internals/ScrapeCard";
20
20
+
import { useIsMobile } from "@/hooks/useIsMobile";
18
21
import {
19
22
ScrapingItems,
20
23
ScrapingSegment,
···
23
26
} from "@/hooks/useProviderScrape";
24
27
import { playerStatus } from "@/stores/player/slices/source";
25
28
import { usePlayerStore } from "@/stores/player/store";
29
29
+
import { usePreferencesStore } from "@/stores/preferences";
30
30
+
31
31
+
interface ScrapingMediaDetails {
32
32
+
voteAverage: number | null;
33
33
+
genres: string[];
34
34
+
}
26
35
27
36
export interface ScrapingProps {
28
37
media: ScrapeMedia;
···
43
52
const setStatus = usePlayerStore((s) => s.setStatus);
44
53
const addFailedSource = usePlayerStore((s) => s.addFailedSource);
45
54
const sourceId = usePlayerStore((s) => s.sourceId);
55
55
+
const meta = usePlayerStore((s) => s.meta);
56
56
+
const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay);
57
57
+
const enableImageLogos = usePreferencesStore((s) => s.enableImageLogos);
58
58
+
const { isMobile } = useIsMobile();
59
59
+
60
60
+
const showMediaColumn = enablePauseOverlay && !isMobile && !!meta;
61
61
+
const [logoUrl, setLogoUrl] = useState<string | null>(null);
62
62
+
const [details, setDetails] = useState<ScrapingMediaDetails>({
63
63
+
voteAverage: null,
64
64
+
genres: [],
65
65
+
});
66
66
+
67
67
+
useEffect(() => {
68
68
+
if (!showMediaColumn || !meta?.tmdbId) return;
69
69
+
let mounted = true;
70
70
+
const fetchLogo = async () => {
71
71
+
if (!enableImageLogos) {
72
72
+
setLogoUrl(null);
73
73
+
return;
74
74
+
}
75
75
+
try {
76
76
+
const type =
77
77
+
meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
78
78
+
const url = await getMediaLogo(meta.tmdbId, type);
79
79
+
if (mounted) setLogoUrl(url || null);
80
80
+
} catch {
81
81
+
if (mounted) setLogoUrl(null);
82
82
+
}
83
83
+
};
84
84
+
fetchLogo();
85
85
+
return () => {
86
86
+
mounted = false;
87
87
+
};
88
88
+
}, [showMediaColumn, meta?.tmdbId, meta?.type, enableImageLogos]);
89
89
+
90
90
+
useEffect(() => {
91
91
+
if (!showMediaColumn || !meta?.tmdbId) return;
92
92
+
let mounted = true;
93
93
+
const fetchDetails = async () => {
94
94
+
try {
95
95
+
const type =
96
96
+
meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
97
97
+
const data = await getMediaDetails(meta.tmdbId, type, false);
98
98
+
if (mounted && data) {
99
99
+
const voteAverage =
100
100
+
typeof data.vote_average === "number" ? data.vote_average : null;
101
101
+
const genres = (data.genres ?? []).map(
102
102
+
(g: { name: string }) => g.name,
103
103
+
);
104
104
+
setDetails({ voteAverage, genres });
105
105
+
}
106
106
+
} catch {
107
107
+
if (mounted) setDetails({ voteAverage: null, genres: [] });
108
108
+
}
109
109
+
};
110
110
+
fetchDetails();
111
111
+
return () => {
112
112
+
mounted = false;
113
113
+
};
114
114
+
}, [showMediaColumn, meta?.tmdbId, meta?.type]);
46
115
47
116
const containerRef = useRef<HTMLDivElement | null>(null);
48
117
const listRef = useRef<HTMLDivElement | null>(null);
···
125
194
if (currentProviderIndex === -1)
126
195
currentProviderIndex = sourceOrder.length - 1;
127
196
197
197
+
const overview =
198
198
+
meta && (meta.type === "show" ? meta.episode?.overview : meta.overview);
199
199
+
const hasMediaDetails =
200
200
+
details.voteAverage !== null || details.genres.length > 0;
201
201
+
const hasMediaContent =
202
202
+
showMediaColumn &&
203
203
+
meta &&
204
204
+
(overview || logoUrl || meta.title || hasMediaDetails);
205
205
+
128
206
return (
129
207
<div
130
130
-
className="h-full w-full relative dir-neutral:origin-top-left flex"
208
208
+
className={classNames(
209
209
+
"h-full w-full relative dir-neutral:origin-top-left flex",
210
210
+
showMediaColumn && "gap-8 lg:gap-12",
211
211
+
)}
131
212
ref={containerRef}
132
213
>
133
133
-
{!sourceOrder || sourceOrder.length === 0 ? (
134
134
-
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0">
135
135
-
<Loading className="mb-8" />
136
136
-
<p>{t("player.scraping.items.pending")}</p>
214
214
+
{showMediaColumn && hasMediaContent && (
215
215
+
<div className="flex-shrink-0 w-80 max-w-[min(20rem,40%)] flex items-center py-6">
216
216
+
<div className="max-w-sm">
217
217
+
{logoUrl ? (
218
218
+
<img
219
219
+
src={logoUrl}
220
220
+
alt={meta.title}
221
221
+
className="mb-6 max-h-32 object-contain drop-shadow-lg"
222
222
+
/>
223
223
+
) : (
224
224
+
<h1 className="mb-4 text-4xl font-bold text-white drop-shadow-lg">
225
225
+
{meta.title}
226
226
+
</h1>
227
227
+
)}
228
228
+
229
229
+
{meta.type === "show" && meta.episode && (
230
230
+
<h2 className="mb-2 text-2xl font-semibold text-white/90 drop-shadow-md">
231
231
+
{meta.episode.title}
232
232
+
</h2>
233
233
+
)}
234
234
+
235
235
+
{hasMediaDetails && (
236
236
+
<div className="mb-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-white/80 drop-shadow-md">
237
237
+
{details.voteAverage !== null && (
238
238
+
<span>
239
239
+
{details.voteAverage.toFixed(1)}
240
240
+
<span className="text-white/60 ml-0.5">/10</span>
241
241
+
</span>
242
242
+
)}
243
243
+
{details.genres.length > 0 && (
244
244
+
<>
245
245
+
{details.voteAverage !== null && (
246
246
+
<span className="text-white/60">•</span>
247
247
+
)}
248
248
+
<span>{details.genres.slice(0, 4).join(", ")}</span>
249
249
+
</>
250
250
+
)}
251
251
+
</div>
252
252
+
)}
253
253
+
254
254
+
{overview && (
255
255
+
<p className="text-lg text-white/80 drop-shadow-md line-clamp-6">
256
256
+
{overview}
257
257
+
</p>
258
258
+
)}
259
259
+
</div>
137
260
</div>
138
138
-
) : null}
139
139
-
<div
140
140
-
className={classNames({
141
141
-
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0": true,
142
142
-
"!opacity-100": renderedOnce,
143
143
-
})}
144
144
-
ref={listRef}
145
145
-
>
146
146
-
{sourceOrder.map((order) => {
147
147
-
const source = sources[order.id];
148
148
-
const distance = Math.abs(
149
149
-
sourceOrder.findIndex((o) => o.id === order.id) -
150
150
-
currentProviderIndex,
151
151
-
);
152
152
-
return (
153
153
-
<div
154
154
-
className="transition-opacity duration-100"
155
155
-
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
156
156
-
key={order.id}
157
157
-
>
158
158
-
<ScrapeCard
159
159
-
id={order.id}
160
160
-
name={source.name}
161
161
-
status={source.status}
162
162
-
hasChildren={order.children.length > 0}
163
163
-
percentage={source.percentage}
261
261
+
)}
262
262
+
263
263
+
<div className="flex-1 min-w-0 relative flex">
264
264
+
{!sourceOrder || sourceOrder.length === 0 ? (
265
265
+
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0">
266
266
+
<Loading className="mb-8" />
267
267
+
<p>{t("player.scraping.items.pending")}</p>
268
268
+
</div>
269
269
+
) : null}
270
270
+
<div
271
271
+
className={classNames({
272
272
+
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0": true,
273
273
+
"!opacity-100": renderedOnce,
274
274
+
})}
275
275
+
ref={listRef}
276
276
+
>
277
277
+
{sourceOrder.map((order) => {
278
278
+
const source = sources[order.id];
279
279
+
const distance = Math.abs(
280
280
+
sourceOrder.findIndex((o) => o.id === order.id) -
281
281
+
currentProviderIndex,
282
282
+
);
283
283
+
return (
284
284
+
<div
285
285
+
className="transition-opacity duration-100"
286
286
+
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
287
287
+
key={order.id}
164
288
>
165
165
-
<div
166
166
-
className={classNames({
167
167
-
"space-y-6 mt-8": order.children.length > 0,
168
168
-
})}
289
289
+
<ScrapeCard
290
290
+
id={order.id}
291
291
+
name={source.name}
292
292
+
status={source.status}
293
293
+
hasChildren={order.children.length > 0}
294
294
+
percentage={source.percentage}
169
295
>
170
170
-
{order.children.map((embedId) => {
171
171
-
const embed = sources[embedId];
172
172
-
return (
173
173
-
<ScrapeItem
174
174
-
id={embedId}
175
175
-
name={embed.name}
176
176
-
status={embed.status}
177
177
-
percentage={embed.percentage}
178
178
-
key={embedId}
179
179
-
/>
180
180
-
);
181
181
-
})}
182
182
-
</div>
183
183
-
</ScrapeCard>
184
184
-
</div>
185
185
-
);
186
186
-
})}
296
296
+
<div
297
297
+
className={classNames({
298
298
+
"space-y-6 mt-8": order.children.length > 0,
299
299
+
})}
300
300
+
>
301
301
+
{order.children.map((embedId) => {
302
302
+
const embed = sources[embedId];
303
303
+
return (
304
304
+
<ScrapeItem
305
305
+
id={embedId}
306
306
+
name={embed.name}
307
307
+
status={embed.status}
308
308
+
percentage={embed.percentage}
309
309
+
key={embedId}
310
310
+
/>
311
311
+
);
312
312
+
})}
313
313
+
</div>
314
314
+
</ScrapeCard>
315
315
+
</div>
316
316
+
);
317
317
+
})}
318
318
+
</div>
187
319
</div>
188
320
</div>
189
321
);