vidply 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +946 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
- package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
- package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
- package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
- package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
- package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
- package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
- package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
- package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
- package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
- package/dist/dev/vidply.esm.js +73 -34
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
- package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
- package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
- package/dist/legacy/vidply.js +345 -32
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +66 -47
- package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
- package/dist/vidply.css +5796 -5796
- package/dist/vidply.esm.min.meta.json +148 -135
- package/package.json +7 -7
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4159 -4159
- package/src/controls/KeyboardManager.ts +383 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2915 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +217 -217
- package/src/i18n/languages/en.ts +218 -218
- package/src/i18n/languages/es.ts +214 -214
- package/src/i18n/languages/fr.ts +214 -214
- package/src/i18n/languages/ja.ts +214 -214
- package/src/icons/Icons.ts +167 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5796 -5796
- package/src/types/events.ts +124 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +185 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-YVCYQZJI.js.map} +0 -0
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Download metadata helpers
|
|
3
|
-
*
|
|
4
|
-
* Used by the download button to resolve a human-readable file format
|
|
5
|
-
* (e.g. MP4, MP3, WebM) and size string (e.g. 12.4 MB) for the button's
|
|
6
|
-
* aria-label and tooltip.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const MIME_TO_FORMAT: Record<string, string> = {
|
|
10
|
-
'video/mp4': 'MP4',
|
|
11
|
-
'video/webm': 'WebM',
|
|
12
|
-
'video/ogg': 'Ogg',
|
|
13
|
-
'video/quicktime': 'MOV',
|
|
14
|
-
'video/x-matroska': 'MKV',
|
|
15
|
-
'video/x-msvideo': 'AVI',
|
|
16
|
-
'audio/mpeg': 'MP3',
|
|
17
|
-
'audio/mp3': 'MP3',
|
|
18
|
-
'audio/mp4': 'M4A',
|
|
19
|
-
'audio/x-m4a': 'M4A',
|
|
20
|
-
'audio/aac': 'AAC',
|
|
21
|
-
'audio/ogg': 'Ogg',
|
|
22
|
-
'audio/opus': 'Opus',
|
|
23
|
-
'audio/wav': 'WAV',
|
|
24
|
-
'audio/x-wav': 'WAV',
|
|
25
|
-
'audio/wave': 'WAV',
|
|
26
|
-
'audio/flac': 'FLAC',
|
|
27
|
-
'audio/x-flac': 'FLAC',
|
|
28
|
-
'audio/webm': 'WebM'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const EXT_TO_FORMAT: Record<string, string> = {
|
|
32
|
-
mp4: 'MP4',
|
|
33
|
-
m4v: 'MP4',
|
|
34
|
-
mov: 'MOV',
|
|
35
|
-
webm: 'WebM',
|
|
36
|
-
mkv: 'MKV',
|
|
37
|
-
avi: 'AVI',
|
|
38
|
-
ogv: 'Ogg',
|
|
39
|
-
ogg: 'Ogg',
|
|
40
|
-
oga: 'Ogg',
|
|
41
|
-
mp3: 'MP3',
|
|
42
|
-
m4a: 'M4A',
|
|
43
|
-
aac: 'AAC',
|
|
44
|
-
opus: 'Opus',
|
|
45
|
-
wav: 'WAV',
|
|
46
|
-
flac: 'FLAC'
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export function inferFormatFromMime(mime: string | null | undefined): string | null {
|
|
50
|
-
if (!mime) return null;
|
|
51
|
-
const trimmed = (mime.split(';')[0] ?? '').trim().toLowerCase();
|
|
52
|
-
return MIME_TO_FORMAT[trimmed] || null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function inferFormatFromUrl(url: string | null | undefined): string | null {
|
|
56
|
-
if (!url) return null;
|
|
57
|
-
try {
|
|
58
|
-
const cleaned = url.split('?')[0]?.split('#')[0] ?? '';
|
|
59
|
-
const lastSegment = cleaned.split('/').pop() || '';
|
|
60
|
-
const dotIndex = lastSegment.lastIndexOf('.');
|
|
61
|
-
if (dotIndex < 0 || dotIndex === lastSegment.length - 1) return null;
|
|
62
|
-
const ext = lastSegment.slice(dotIndex + 1).toLowerCase();
|
|
63
|
-
return EXT_TO_FORMAT[ext] || null;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function formatBytes(bytes: number, locale: string = 'en'): string | null {
|
|
70
|
-
if (!isFinite(bytes) || bytes < 0) return null;
|
|
71
|
-
|
|
72
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
73
|
-
let value = bytes;
|
|
74
|
-
let unitIndex = 0;
|
|
75
|
-
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
76
|
-
value /= 1024;
|
|
77
|
-
unitIndex++;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Bytes/KB get no decimals; MB and up get one decimal.
|
|
81
|
-
const fractionDigits = unitIndex < 2 ? 0 : 1;
|
|
82
|
-
|
|
83
|
-
let formatted: string;
|
|
84
|
-
try {
|
|
85
|
-
formatted = new Intl.NumberFormat(locale, {
|
|
86
|
-
minimumFractionDigits: fractionDigits,
|
|
87
|
-
maximumFractionDigits: fractionDigits
|
|
88
|
-
}).format(value);
|
|
89
|
-
} catch {
|
|
90
|
-
formatted = value.toFixed(fractionDigits);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return `${formatted} ${units[unitIndex]}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface FetchContentLengthOptions {
|
|
97
|
-
signal?: AbortSignal;
|
|
98
|
-
timeoutMs?: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function fetchContentLength(
|
|
102
|
-
url: string,
|
|
103
|
-
options: FetchContentLengthOptions = {}
|
|
104
|
-
): Promise<number | null> {
|
|
105
|
-
if (!url || typeof fetch !== 'function') return null;
|
|
106
|
-
|
|
107
|
-
// Compose abort signals: caller's lifecycle signal + a per-call timeout.
|
|
108
|
-
const signals: AbortSignal[] = [];
|
|
109
|
-
if (options.signal) signals.push(options.signal);
|
|
110
|
-
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
111
|
-
signals.push(AbortSignal.timeout(options.timeoutMs ?? 8000));
|
|
112
|
-
}
|
|
113
|
-
let combinedSignal: AbortSignal | undefined;
|
|
114
|
-
if (signals.length === 1) combinedSignal = signals[0];
|
|
115
|
-
else if (signals.length > 1) {
|
|
116
|
-
const anyFn = (AbortSignal as { any?: (s: AbortSignal[]) => AbortSignal }).any;
|
|
117
|
-
combinedSignal = anyFn ? anyFn(signals) : signals[0];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const response = await fetch(url, {
|
|
122
|
-
method: 'HEAD',
|
|
123
|
-
credentials: 'omit',
|
|
124
|
-
cache: 'no-store',
|
|
125
|
-
signal: combinedSignal
|
|
126
|
-
});
|
|
127
|
-
if (!response.ok) return null;
|
|
128
|
-
const header = response.headers.get('Content-Length');
|
|
129
|
-
if (!header) return null;
|
|
130
|
-
const size = Number(header);
|
|
131
|
-
return Number.isFinite(size) && size > 0 ? size : null;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
// CORS, network errors, abort, or HEAD not allowed: silently give up.
|
|
134
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
135
|
-
console.debug('[vidply] HEAD request for download size failed:', error);
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface DownloadLabelParts {
|
|
142
|
-
baseLabel: string;
|
|
143
|
-
format?: string | null;
|
|
144
|
-
sizeBytes?: number | null;
|
|
145
|
-
locale?: string;
|
|
146
|
-
withFormatSizeTemplate: string;
|
|
147
|
-
withFormatTemplate: string;
|
|
148
|
-
withSizeTemplate: string;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Compose the localized download label.
|
|
153
|
-
* Templates are i18n strings already containing {format} / {size} placeholders.
|
|
154
|
-
*/
|
|
155
|
-
export function buildDownloadLabel(parts: DownloadLabelParts): string {
|
|
156
|
-
const { baseLabel, format, sizeBytes, locale = 'en' } = parts;
|
|
157
|
-
const sizeStr = sizeBytes != null ? formatBytes(sizeBytes, locale) : null;
|
|
158
|
-
|
|
159
|
-
if (format && sizeStr) {
|
|
160
|
-
return parts.withFormatSizeTemplate
|
|
161
|
-
.replace('{format}', format)
|
|
162
|
-
.replace('{size}', sizeStr);
|
|
163
|
-
}
|
|
164
|
-
if (format) {
|
|
165
|
-
return parts.withFormatTemplate.replace('{format}', format);
|
|
166
|
-
}
|
|
167
|
-
if (sizeStr) {
|
|
168
|
-
return parts.withSizeTemplate.replace('{size}', sizeStr);
|
|
169
|
-
}
|
|
170
|
-
return baseLabel;
|
|
171
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Download metadata helpers
|
|
3
|
+
*
|
|
4
|
+
* Used by the download button to resolve a human-readable file format
|
|
5
|
+
* (e.g. MP4, MP3, WebM) and size string (e.g. 12.4 MB) for the button's
|
|
6
|
+
* aria-label and tooltip.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MIME_TO_FORMAT: Record<string, string> = {
|
|
10
|
+
'video/mp4': 'MP4',
|
|
11
|
+
'video/webm': 'WebM',
|
|
12
|
+
'video/ogg': 'Ogg',
|
|
13
|
+
'video/quicktime': 'MOV',
|
|
14
|
+
'video/x-matroska': 'MKV',
|
|
15
|
+
'video/x-msvideo': 'AVI',
|
|
16
|
+
'audio/mpeg': 'MP3',
|
|
17
|
+
'audio/mp3': 'MP3',
|
|
18
|
+
'audio/mp4': 'M4A',
|
|
19
|
+
'audio/x-m4a': 'M4A',
|
|
20
|
+
'audio/aac': 'AAC',
|
|
21
|
+
'audio/ogg': 'Ogg',
|
|
22
|
+
'audio/opus': 'Opus',
|
|
23
|
+
'audio/wav': 'WAV',
|
|
24
|
+
'audio/x-wav': 'WAV',
|
|
25
|
+
'audio/wave': 'WAV',
|
|
26
|
+
'audio/flac': 'FLAC',
|
|
27
|
+
'audio/x-flac': 'FLAC',
|
|
28
|
+
'audio/webm': 'WebM'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const EXT_TO_FORMAT: Record<string, string> = {
|
|
32
|
+
mp4: 'MP4',
|
|
33
|
+
m4v: 'MP4',
|
|
34
|
+
mov: 'MOV',
|
|
35
|
+
webm: 'WebM',
|
|
36
|
+
mkv: 'MKV',
|
|
37
|
+
avi: 'AVI',
|
|
38
|
+
ogv: 'Ogg',
|
|
39
|
+
ogg: 'Ogg',
|
|
40
|
+
oga: 'Ogg',
|
|
41
|
+
mp3: 'MP3',
|
|
42
|
+
m4a: 'M4A',
|
|
43
|
+
aac: 'AAC',
|
|
44
|
+
opus: 'Opus',
|
|
45
|
+
wav: 'WAV',
|
|
46
|
+
flac: 'FLAC'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function inferFormatFromMime(mime: string | null | undefined): string | null {
|
|
50
|
+
if (!mime) return null;
|
|
51
|
+
const trimmed = (mime.split(';')[0] ?? '').trim().toLowerCase();
|
|
52
|
+
return MIME_TO_FORMAT[trimmed] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function inferFormatFromUrl(url: string | null | undefined): string | null {
|
|
56
|
+
if (!url) return null;
|
|
57
|
+
try {
|
|
58
|
+
const cleaned = url.split('?')[0]?.split('#')[0] ?? '';
|
|
59
|
+
const lastSegment = cleaned.split('/').pop() || '';
|
|
60
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
61
|
+
if (dotIndex < 0 || dotIndex === lastSegment.length - 1) return null;
|
|
62
|
+
const ext = lastSegment.slice(dotIndex + 1).toLowerCase();
|
|
63
|
+
return EXT_TO_FORMAT[ext] || null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatBytes(bytes: number, locale: string = 'en'): string | null {
|
|
70
|
+
if (!isFinite(bytes) || bytes < 0) return null;
|
|
71
|
+
|
|
72
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
73
|
+
let value = bytes;
|
|
74
|
+
let unitIndex = 0;
|
|
75
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
76
|
+
value /= 1024;
|
|
77
|
+
unitIndex++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Bytes/KB get no decimals; MB and up get one decimal.
|
|
81
|
+
const fractionDigits = unitIndex < 2 ? 0 : 1;
|
|
82
|
+
|
|
83
|
+
let formatted: string;
|
|
84
|
+
try {
|
|
85
|
+
formatted = new Intl.NumberFormat(locale, {
|
|
86
|
+
minimumFractionDigits: fractionDigits,
|
|
87
|
+
maximumFractionDigits: fractionDigits
|
|
88
|
+
}).format(value);
|
|
89
|
+
} catch {
|
|
90
|
+
formatted = value.toFixed(fractionDigits);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return `${formatted} ${units[unitIndex]}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface FetchContentLengthOptions {
|
|
97
|
+
signal?: AbortSignal;
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function fetchContentLength(
|
|
102
|
+
url: string,
|
|
103
|
+
options: FetchContentLengthOptions = {}
|
|
104
|
+
): Promise<number | null> {
|
|
105
|
+
if (!url || typeof fetch !== 'function') return null;
|
|
106
|
+
|
|
107
|
+
// Compose abort signals: caller's lifecycle signal + a per-call timeout.
|
|
108
|
+
const signals: AbortSignal[] = [];
|
|
109
|
+
if (options.signal) signals.push(options.signal);
|
|
110
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
111
|
+
signals.push(AbortSignal.timeout(options.timeoutMs ?? 8000));
|
|
112
|
+
}
|
|
113
|
+
let combinedSignal: AbortSignal | undefined;
|
|
114
|
+
if (signals.length === 1) combinedSignal = signals[0];
|
|
115
|
+
else if (signals.length > 1) {
|
|
116
|
+
const anyFn = (AbortSignal as { any?: (s: AbortSignal[]) => AbortSignal }).any;
|
|
117
|
+
combinedSignal = anyFn ? anyFn(signals) : signals[0];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(url, {
|
|
122
|
+
method: 'HEAD',
|
|
123
|
+
credentials: 'omit',
|
|
124
|
+
cache: 'no-store',
|
|
125
|
+
signal: combinedSignal
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) return null;
|
|
128
|
+
const header = response.headers.get('Content-Length');
|
|
129
|
+
if (!header) return null;
|
|
130
|
+
const size = Number(header);
|
|
131
|
+
return Number.isFinite(size) && size > 0 ? size : null;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// CORS, network errors, abort, or HEAD not allowed: silently give up.
|
|
134
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
135
|
+
console.debug('[vidply] HEAD request for download size failed:', error);
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface DownloadLabelParts {
|
|
142
|
+
baseLabel: string;
|
|
143
|
+
format?: string | null;
|
|
144
|
+
sizeBytes?: number | null;
|
|
145
|
+
locale?: string;
|
|
146
|
+
withFormatSizeTemplate: string;
|
|
147
|
+
withFormatTemplate: string;
|
|
148
|
+
withSizeTemplate: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compose the localized download label.
|
|
153
|
+
* Templates are i18n strings already containing {format} / {size} placeholders.
|
|
154
|
+
*/
|
|
155
|
+
export function buildDownloadLabel(parts: DownloadLabelParts): string {
|
|
156
|
+
const { baseLabel, format, sizeBytes, locale = 'en' } = parts;
|
|
157
|
+
const sizeStr = sizeBytes != null ? formatBytes(sizeBytes, locale) : null;
|
|
158
|
+
|
|
159
|
+
if (format && sizeStr) {
|
|
160
|
+
return parts.withFormatSizeTemplate
|
|
161
|
+
.replace('{format}', format)
|
|
162
|
+
.replace('{size}', sizeStr);
|
|
163
|
+
}
|
|
164
|
+
if (format) {
|
|
165
|
+
return parts.withFormatTemplate.replace('{format}', format);
|
|
166
|
+
}
|
|
167
|
+
if (sizeStr) {
|
|
168
|
+
return parts.withSizeTemplate.replace('{size}', sizeStr);
|
|
169
|
+
}
|
|
170
|
+
return baseLabel;
|
|
171
|
+
}
|