pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/

add downloading feature for desktop (#93)

Co-authored-by: Duplicake-fyi <duplicake@pstream.mov>

authored by

Duplicake
Duplicake-fyi
and committed by
GitHub
a53a89a5 f1af25bf

+156 -25
+18 -10
src/components/LinksDropdown.tsx
··· 294 294 {t("navigation.menu.settings")} 295 295 </DropdownLink> 296 296 {isDesktopApp && ( 297 - <DropdownLink 298 - onClick={() => 299 - window.dispatchEvent( 300 - new CustomEvent("pstream-desktop-settings"), 301 - ) 302 - } 303 - icon={Icons.GEAR} 304 - > 305 - {t("navigation.menu.desktop")} 306 - </DropdownLink> 297 + <> 298 + <DropdownLink 299 + onClick={() => 300 + window.dispatchEvent( 301 + new CustomEvent("pstream-desktop-settings"), 302 + ) 303 + } 304 + icon={Icons.GEAR} 305 + > 306 + {t("navigation.menu.desktop")} 307 + </DropdownLink> 308 + <DropdownLink 309 + onClick={() => window.desktopApi?.openOffline()} 310 + icon={Icons.DOWNLOAD} 311 + > 312 + Offline Downloads 313 + </DropdownLink> 314 + </> 307 315 )} 308 316 <DropdownLink href="/watch-history" icon={Icons.CLOCK}> 309 317 {t("home.watchHistory.sectionTitle")}
+127 -15
src/components/player/atoms/settings/Downloads.tsx
··· 2 2 import { Trans, useTranslation } from "react-i18next"; 3 3 import { useCopyToClipboard } from "react-use"; 4 4 5 + import { downloadCaption } from "@/backend/helpers/subs"; 5 6 import { Button } from "@/components/buttons/Button"; 6 7 import { Icon, Icons } from "@/components/Icon"; 7 8 import { OverlayPage } from "@/components/overlays/OverlayPage"; 8 9 import { Menu } from "@/components/player/internals/ContextMenu"; 9 10 import { convertSubtitlesToSrtDataurl } from "@/components/player/utils/captions"; 11 + import { useIsDesktopApp } from "@/hooks/useIsDesktopApp"; 10 12 import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 11 13 import { usePlayerStore } from "@/stores/player/store"; 12 14 ··· 64 66 65 67 const sourceType = usePlayerStore((s) => s.source?.type); 66 68 const selectedCaption = usePlayerStore((s) => s.caption?.selected); 69 + const captionList = usePlayerStore((s) => s.captionList); 70 + const meta = usePlayerStore((s) => s.meta); 71 + const duration = usePlayerStore((s) => s.progress.duration); 72 + const isDesktopApp = useIsDesktopApp(); 73 + 74 + const startOfflineDownload = useCallback(async () => { 75 + if (!downloadUrl) return; 76 + const title = meta?.title ? meta.title : "Video"; 77 + const poster = meta?.poster; 78 + let subtitleText = null; 79 + 80 + if (selectedCaption?.srtData) { 81 + subtitleText = selectedCaption.srtData; 82 + } else if (captionList.length > 0) { 83 + // Auto-fetch the first English caption, or the first available one 84 + const defaultCaption = 85 + captionList.find((c) => c.language === "en") ?? captionList[0]; 86 + try { 87 + subtitleText = await downloadCaption(defaultCaption); 88 + } catch { 89 + // Continue without subtitles if fetch fails 90 + } 91 + } 92 + 93 + window.desktopApi?.startDownload({ 94 + url: downloadUrl, 95 + title, 96 + poster, 97 + subtitleText, 98 + duration, 99 + type: sourceType, 100 + }); 101 + 102 + if (window.desktopApi?.openOffline) { 103 + window.desktopApi.openOffline(); 104 + } else { 105 + router.navigate("/"); 106 + } 107 + }, [ 108 + downloadUrl, 109 + meta, 110 + selectedCaption, 111 + captionList, 112 + duration, 113 + router, 114 + sourceType, 115 + ]); 116 + 67 117 const openSubtitleDownload = useCallback(() => { 68 118 const dataUrl = selectedCaption 69 119 ? convertSubtitlesToSrtDataurl(selectedCaption?.srtData) ··· 83 133 <div className="mb-4"> 84 134 {sourceType === "hls" ? ( 85 135 <div className="mb-6"> 86 - <Menu.Paragraph marginClass="mb-6"> 87 - <StyleTrans k="player.menus.downloads.hlsDisclaimer" /> 88 - </Menu.Paragraph> 136 + {isDesktopApp ? ( 137 + <> 138 + <Menu.Paragraph marginClass="mb-6"> 139 + <Trans i18nKey="player.menus.downloads.desktopDisclaimer"> 140 + Download this video directly to your app for offline 141 + playback. 142 + </Trans> 143 + </Menu.Paragraph> 144 + <Button 145 + className="w-full mt-2" 146 + theme="purple" 147 + onClick={startOfflineDownload} 148 + > 149 + {t( 150 + "player.menus.downloads.offlineButton", 151 + "Download for Offline Use", 152 + )} 153 + </Button> 154 + </> 155 + ) : ( 156 + <> 157 + <Menu.Paragraph marginClass="mb-6"> 158 + <StyleTrans k="player.menus.downloads.hlsDisclaimer" /> 159 + </Menu.Paragraph> 89 160 90 - <Button className="w-full mt-2" theme="purple" href={hlsDownload}> 91 - {t("player.menus.downloads.button")} 92 - </Button> 93 - <p className="text-xs py-4"> 94 - <Trans i18nKey="player.menus.downloads.hlsDownloader"> 95 - <a 96 - className="text-type-link" 97 - href="https://hls-downloader.pstream.mov/" 98 - /> 99 - </Trans> 100 - </p> 161 + <Button 162 + className="w-full mt-2" 163 + theme="purple" 164 + href={hlsDownload} 165 + > 166 + {t("player.menus.downloads.button")} 167 + </Button> 168 + <p className="text-xs py-4"> 169 + <Trans i18nKey="player.menus.downloads.hlsDownloader"> 170 + <a 171 + className="text-type-link" 172 + href="https://hls-downloader.pstream.mov/" 173 + /> 174 + </Trans> 175 + </p> 176 + </> 177 + )} 101 178 <Button 102 179 className="w-full mt-2" 103 180 theme="secondary" ··· 120 197 {t("player.menus.downloads.downloadSubtitle")} 121 198 </Button> 122 199 </div> 200 + ) : sourceType === "file" ? ( 201 + <div className="mb-6"> 202 + {isDesktopApp ? ( 203 + <> 204 + <Menu.Paragraph marginClass="mb-6"> 205 + <Trans i18nKey="player.menus.downloads.desktopDisclaimer"> 206 + Download this video directly to your app for offline 207 + playback. 208 + </Trans> 209 + </Menu.Paragraph> 210 + <Button 211 + className="w-full mt-2" 212 + theme="purple" 213 + onClick={startOfflineDownload} 214 + > 215 + {t( 216 + "player.menus.downloads.offlineButton", 217 + "Download for Offline Use", 218 + )} 219 + </Button> 220 + </> 221 + ) : ( 222 + <Button className="w-full" href={downloadUrl} theme="purple"> 223 + {t("player.menus.downloads.downloadVideo")} 224 + </Button> 225 + )} 226 + <Button 227 + className="w-full mt-2" 228 + onClick={openSubtitleDownload} 229 + disabled={!selectedCaption} 230 + theme="secondary" 231 + download="subtitles.srt" 232 + > 233 + {t("player.menus.downloads.downloadSubtitle")} 234 + </Button> 235 + </div> 123 236 ) : ( 124 237 <> 125 238 <Menu.ChevronLink onClick={() => router.navigate("/download/pc")}> ··· 141 254 <Menu.Paragraph marginClass="my-6"> 142 255 <StyleTrans k="player.menus.downloads.disclaimer" /> 143 256 </Menu.Paragraph> 144 - 145 257 <Button className="w-full" href={downloadUrl} theme="purple"> 146 258 {t("player.menus.downloads.downloadVideo")} 147 259 </Button>
+11
src/hooks/useIsDesktopApp.ts
··· 2 2 declare global { 3 3 interface Window { 4 4 __PSTREAM_DESKTOP__?: boolean; 5 + desktopApi?: { 6 + startDownload(data: { 7 + url: string; 8 + title: string; 9 + poster?: string; 10 + subtitleText?: string; 11 + duration?: number; 12 + type?: string; 13 + }): void; 14 + openOffline(): void; 15 + }; 5 16 } 6 17 } 7 18