···167167 <h1>Branding Administration</h1>
168168 <p class="subtitle">Customize your Streamplace instance</p>
169169170170+ <!-- Broadcaster DID -->
171171+ <div class="section">
172172+ <div class="section-title">Broadcaster DID</div>
173173+ <div class="section-subtitle">Leave empty to use server default</div>
174174+ <div class="input-group">
175175+ <input
176176+ type="text"
177177+ id="broadcasterDIDInput"
178178+ placeholder="did:plc:..."
179179+ value=""
180180+ />
181181+ </div>
182182+ </div>
183183+170184 <!-- Site Title -->
171185 <div class="section">
172186 <div class="section-title">Site Title</div>
···293307 <script>
294308 let currentBranding = {};
295309310310+ function getBroadcasterDID() {
311311+ return document.getElementById("broadcasterDIDInput").value.trim();
312312+ }
313313+314314+ function getBroadcasterParam() {
315315+ const did = getBroadcasterDID();
316316+ return did ? `?broadcaster=${encodeURIComponent(did)}` : "";
317317+ }
318318+296319 async function loadBranding() {
297320 try {
298321 // fetch branding metadata
299322 const response = await fetch(
300300- "/xrpc/place.stream.branding.getBranding",
323323+ `/xrpc/place.stream.branding.getBranding${getBroadcasterParam()}`,
301324 );
302325 if (!response.ok) {
303326 throw new Error("Failed to load branding");
···306329 const data = await response.json();
307330 currentBranding = {};
308331309309- // fetch actual data for text assets
332332+ // process assets - use inline data for text, URLs for images
310333 for (const asset of data.assets) {
311311- if (asset.mimeType === "text/plain") {
312312- try {
313313- const blobResponse = await fetch(asset.url);
314314- if (blobResponse.ok) {
315315- currentBranding[asset.key] = await blobResponse.text();
316316- }
317317- } catch (e) {
318318- console.warn(`Failed to fetch ${asset.key}:`, e);
319319- }
320320- } else {
321321- // for images, store the URL
334334+ if (asset.data) {
335335+ // text asset with inline data
336336+ currentBranding[asset.key] = asset.data;
337337+ } else if (asset.url) {
338338+ // image asset with URL
322339 currentBranding[asset.key] = asset.url;
323340 }
324341 }
···370387 }
371388372389 try {
373373- const response = await fetch(`/branding/${key}`, {
374374- method: "PUT",
375375- headers: {
376376- "Content-Type": "text/plain",
390390+ const response = await fetch(
391391+ `/branding/${key}${getBroadcasterParam()}`,
392392+ {
393393+ method: "PUT",
394394+ headers: {
395395+ "Content-Type": "text/plain",
396396+ },
397397+ body: value.trim(),
377398 },
378378- body: value.trim(),
379379- });
399399+ );
380400381401 if (!response.ok) {
382402 throw new Error("Upload failed: " + response.statusText);
···402422 }
403423404424 try {
405405- const response = await fetch(`/branding/${key}`, {
406406- method: "PUT",
407407- headers: {
408408- "Content-Type": file.type,
425425+ const response = await fetch(
426426+ `/branding/${key}${getBroadcasterParam()}`,
427427+ {
428428+ method: "PUT",
429429+ headers: {
430430+ "Content-Type": file.type,
431431+ },
432432+ body: file,
409433 },
410410- body: file,
411411- });
434434+ );
412435413436 if (!response.ok) {
414437 throw new Error("Upload failed: " + response.statusText);
···432455 }
433456434457 try {
435435- const response = await fetch(`/branding/${key}`, {
436436- method: "DELETE",
437437- });
458458+ const response = await fetch(
459459+ `/branding/${key}${getBroadcasterParam()}`,
460460+ {
461461+ method: "DELETE",
462462+ },
463463+ );
438464439465 if (!response.ok) {
440466 throw new Error("Delete failed: " + response.statusText);
+14-7
pkg/spxrpc/place_stream_branding.go
···116116 // build output
117117 assets := make([]*placestreamtypes.BrandingGetBranding_BrandingAsset, 0, len(allKeys))
118118 for key := range allKeys {
119119- _, mimeType, err := s.getBrandingBlobCached(ctx, broadcasterID, key)
119119+ data, mimeType, err := s.getBrandingBlobCached(ctx, broadcasterID, key)
120120 if err != nil {
121121 continue // skip if error
122122 }
123123124124- // construct URL - need to get base URL from echo context
125125- url := fmt.Sprintf("/xrpc/place.stream.branding.getBlob?key=%s", key)
126126-127127- assets = append(assets, &placestreamtypes.BrandingGetBranding_BrandingAsset{
124124+ asset := &placestreamtypes.BrandingGetBranding_BrandingAsset{
128125 Key: key,
129126 MimeType: mimeType,
130130- Url: url,
131131- })
127127+ }
128128+129129+ // for text assets, include data inline; for images, provide URL
130130+ if mimeType == "text/plain" {
131131+ str := string(data)
132132+ asset.Data = &str
133133+ } else {
134134+ url := fmt.Sprintf("/xrpc/place.stream.branding.getBlob?key=%s", key)
135135+ asset.Url = &url
136136+ }
137137+138138+ assets = append(assets, asset)
132139 }
133140134141 return &placestreamtypes.BrandingGetBranding_Output{
+4-2
pkg/streamplace/brandinggetBranding.go
···12121313// BrandingGetBranding_BrandingAsset is a "brandingAsset" in the place.stream.branding.getBranding schema.
1414type BrandingGetBranding_BrandingAsset struct {
1515+ // data: Inline data for text assets
1616+ Data *string `json:"data,omitempty" cborgen:"data,omitempty"`
1517 // key: Asset key identifier
1618 Key string `json:"key" cborgen:"key"`
1719 // mimeType: MIME type of the asset
1820 MimeType string `json:"mimeType" cborgen:"mimeType"`
1919- // url: URL to fetch the asset blob
2020- Url string `json:"url" cborgen:"url"`
2121+ // url: URL to fetch the asset blob (for images)
2222+ Url *string `json:"url,omitempty" cborgen:"url,omitempty"`
2123}
22242325// BrandingGetBranding_Output is the output of a place.stream.branding.getBranding call.