forked from pdsls.dev/pdsls
atproto explorer

load blobs from PDS, no more bsky CDN

juli.ee 10b1dfce 066b60c2

verified
Changed files
+34 -93
src
-1
package.json
··· 50 50 "@solidjs/meta": "^0.29.4", 51 51 "@solidjs/router": "^0.15.3", 52 52 "codemirror": "^6.0.2", 53 - "hls.js": "^1.6.13", 54 53 "solid-js": "^1.9.9" 55 54 }, 56 55 "packageManager": "pnpm@10.12.2+sha512.a32540185b964ee30bb4e979e405adc6af59226b438ee4cc19f9e8773667a66d302f5bfee60a39d3cac69e35e4b96e708a71dd002b7e9359c4112a1722ac323f"
-8
pnpm-lock.yaml
··· 95 95 codemirror: 96 96 specifier: ^6.0.2 97 97 version: 6.0.2 98 - hls.js: 99 - specifier: ^1.6.13 100 - version: 1.6.13 101 98 solid-js: 102 99 specifier: ^1.9.9 103 100 version: 1.9.9 ··· 1038 1035 1039 1036 graceful-fs@4.2.11: 1040 1037 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1041 - 1042 - hls.js@1.6.13: 1043 - resolution: {integrity: sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==} 1044 1038 1045 1039 html-entities@2.3.3: 1046 1040 resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} ··· 2274 2268 globals@15.15.0: {} 2275 2269 2276 2270 graceful-fs@4.2.11: {} 2277 - 2278 - hls.js@1.6.13: {} 2279 2271 2280 2272 html-entities@2.3.3: {} 2281 2273
+21 -16
src/components/json.tsx
··· 1 - import { A } from "@solidjs/router"; 2 - import { createEffect, createSignal, For, Show } from "solid-js"; 1 + import { A, useParams } from "@solidjs/router"; 2 + import { createEffect, createSignal, ErrorBoundary, For, Show } from "solid-js"; 3 3 import { hideMedia } from "../views/settings"; 4 4 import { pds } from "./navbar"; 5 5 import Tooltip from "./tooltip"; ··· 77 77 }; 78 78 79 79 const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => { 80 - const [hide, setHide] = createSignal(localStorage.hideMedia === "true"); 80 + const params = useParams(); 81 + const [hide, setHide] = createSignal( 82 + localStorage.hideMedia === "true" || params.rkey === undefined, 83 + ); 81 84 82 - createEffect(() => setHide(hideMedia())); 85 + createEffect(() => { 86 + if (hideMedia()) setHide(hideMedia()); 87 + }); 83 88 84 89 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 85 90 const [show, setShow] = createSignal(true); ··· 132 137 <> 133 138 <span class="flex gap-x-1"> 134 139 <Show when={blob.mimeType.startsWith("image/") && !hide()}> 135 - <a 136 - href={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`} 137 - target="_blank" 138 - > 139 - <img 140 - class="max-h-[16rem] w-full max-w-[16rem]" 141 - src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`} 142 - /> 143 - </a> 140 + <img 141 + class="max-h-[16rem] w-fit max-w-[16rem]" 142 + src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`} 143 + /> 144 144 </Show> 145 145 <Show when={blob.mimeType === "video/mp4" && !hide()}> 146 - <VideoPlayer did={repo} cid={blob.ref.$link} /> 146 + <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 147 + <VideoPlayer did={repo} cid={blob.ref.$link} /> 148 + </ErrorBoundary> 147 149 </Show> 148 150 <span 149 - classList={{ "flex items-center justify-between gap-1": true, "flex-col": !hide() }} 151 + classList={{ 152 + "flex items-center justify-between gap-1": true, 153 + "flex-col": !hide(), 154 + }} 150 155 > 151 156 <Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}> 152 157 <Tooltip text={hide() ? "Show" : "Hide"}> ··· 161 166 </Tooltip> 162 167 </Show> 163 168 <Show when={pds()}> 164 - <Tooltip text="Blob PDS link"> 169 + <Tooltip text="Blob on PDS"> 165 170 <a 166 171 href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`} 167 172 target="_blank"
+13 -68
src/components/video-player.tsx
··· 1 - // courtesy of the best 🐇, my lovely sister mary 2 - import Hls from "hls.js"; 3 - import { createEffect, createSignal, onCleanup, Show } from "solid-js"; 1 + import { onMount } from "solid-js"; 2 + import { pds } from "./navbar"; 4 3 5 4 export interface VideoPlayerProps { 6 - /** Expected to be static */ 7 5 did: string; 8 6 cid: string; 9 7 } 10 8 11 9 const VideoPlayer = ({ did, cid }: VideoPlayerProps) => { 12 - const [playing, setPlaying] = createSignal(false); 13 - const [error, setError] = createSignal(false); 10 + let video!: HTMLVideoElement; 14 11 15 - const hls = new Hls({ 16 - capLevelToPlayerSize: true, 17 - startLevel: 1, 18 - xhrSetup(xhr, urlString) { 19 - const url = new URL(urlString); 20 - 21 - // Just in case it fails, we'll remove `session_id` everywhere 22 - url.searchParams.delete("session_id"); 23 - 24 - xhr.open("get", url.toString()); 25 - }, 12 + onMount(async () => { 13 + // thanks bf <3 14 + const res = await fetch(`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`); 15 + if (!res.ok) throw new Error(res.statusText); 16 + const blob = await res.blob(); 17 + const url = URL.createObjectURL(blob); 18 + if (video) video.src = url; 26 19 }); 27 20 28 - onCleanup(() => hls.destroy()); 29 - 30 - hls.loadSource(`https://video.cdn.bsky.app/hls/${did}/${cid}/playlist.m3u8`); 31 - hls.on(Hls.Events.ERROR, () => setError(true)); 32 - 33 21 return ( 34 - <div class="max-w-xs"> 35 - <Show when={!error()}> 36 - <video 37 - ref={(node) => { 38 - hls.attachMedia(node); 39 - 40 - createEffect(() => { 41 - if (!playing()) { 42 - return; 43 - } 44 - 45 - const observer = new IntersectionObserver( 46 - (entries) => { 47 - const entry = entries[0]; 48 - if (!entry.isIntersecting) { 49 - node.pause(); 50 - } 51 - }, 52 - { threshold: 0.5 }, 53 - ); 54 - 55 - onCleanup(() => observer.disconnect()); 56 - 57 - observer.observe(node); 58 - }); 59 - }} 60 - controls 61 - playsinline 62 - onPlay={() => setPlaying(true)} 63 - onPause={() => setPlaying(false)} 64 - onLoadedMetadata={(ev) => { 65 - const video = ev.currentTarget; 66 - 67 - const hasAudio = 68 - // @ts-expect-error: Mozilla-specific 69 - video.mozHasAudio || 70 - // @ts-expect-error: WebKit/Blink-specific 71 - !!video.webkitAudioDecodedByteCount || 72 - // @ts-expect-error: WebKit-specific 73 - !!(video.audioTracks && video.audioTracks.length); 74 - 75 - video.loop = !hasAudio || video.duration <= 6; 76 - }} 77 - /> 78 - </Show> 79 - </div> 22 + <video ref={video} class="max-w-xs" controls playsinline> 23 + <source type="video/mp4" /> 24 + </video> 80 25 ); 81 26 }; 82 27