Monorepo for Aesthetic.Computer aesthetic.computer
at main 190 lines 5.1 kB view raw
1// FF1 Proxy (Netlify Function) 2// Relays FF1 displayPlaylist requests to Feral File cloud relay. 3// 4// NOTE: The Feral File relay (artwork-info.feral-file.workers.dev) may be 5// temporarily unavailable. This function provides two modes: 6// 1. Cloud relay mode (requires topicID + apiKey from FF1 app) 7// 2. Direct mode (requires deviceIp - FF1 must be accessible from server) 8 9const RELAY_BASE = "https://artwork-info.feral-file.workers.dev/api/cast"; 10 11function corsHeaders() { 12 return { 13 "Access-Control-Allow-Origin": "*", 14 "Access-Control-Allow-Methods": "POST, OPTIONS", 15 "Access-Control-Allow-Headers": "Content-Type", 16 }; 17} 18 19export async function handler(event) { 20 if (event.httpMethod === "OPTIONS") { 21 return { 22 statusCode: 204, 23 headers: corsHeaders(), 24 body: "", 25 }; 26 } 27 28 if (event.httpMethod !== "POST") { 29 return { 30 statusCode: 405, 31 headers: corsHeaders(), 32 body: JSON.stringify({ success: false, error: "Method not allowed" }), 33 }; 34 } 35 36 let body; 37 try { 38 body = JSON.parse(event.body || "{}"); 39 } catch (err) { 40 return { 41 statusCode: 400, 42 headers: corsHeaders(), 43 body: JSON.stringify({ success: false, error: "Invalid JSON body" }), 44 }; 45 } 46 47 const { topicID, apiKey, command, request, deviceIp } = body; 48 49 // Mode 1: Direct device connection (if deviceIp provided) 50 if (deviceIp) { 51 const directUrl = `http://${deviceIp}:1111/api/cast`; 52 const payload = { 53 command: command || "displayPlaylist", 54 request, 55 }; 56 57 try { 58 const response = await fetch(directUrl, { 59 method: "POST", 60 headers: { "Content-Type": "application/json" }, 61 body: JSON.stringify(payload), 62 }); 63 64 const responseText = await response.text(); 65 let responseData; 66 try { 67 responseData = JSON.parse(responseText); 68 } catch { 69 responseData = { raw: responseText }; 70 } 71 72 if (!response.ok) { 73 return { 74 statusCode: response.status, 75 headers: corsHeaders(), 76 body: JSON.stringify({ 77 success: false, 78 error: `FF1 direct error: ${response.status}`, 79 details: responseData, 80 }), 81 }; 82 } 83 84 return { 85 statusCode: 200, 86 headers: corsHeaders(), 87 body: JSON.stringify({ success: true, response: responseData, mode: "direct" }), 88 }; 89 } catch (err) { 90 return { 91 statusCode: 500, 92 headers: corsHeaders(), 93 body: JSON.stringify({ 94 success: false, 95 error: `Cannot reach FF1 at ${deviceIp}: ${err.message}`, 96 hint: "FF1 must be accessible from the server (same network or tunneled)" 97 }), 98 }; 99 } 100 } 101 102 // Mode 2: Cloud relay (requires topicID) 103 if (!topicID) { 104 return { 105 statusCode: 400, 106 headers: corsHeaders(), 107 body: JSON.stringify({ 108 success: false, 109 error: "Missing topicID - get this from your FF1 app settings", 110 hint: "Go to FF1 app > Settings > Developer to find your Topic ID", 111 }), 112 }; 113 } 114 115 if (!apiKey) { 116 return { 117 statusCode: 400, 118 headers: corsHeaders(), 119 body: JSON.stringify({ 120 success: false, 121 error: "Missing apiKey - the cloud relay requires an API key", 122 hint: "Get your API key from the FF1 app or contact Feral File support", 123 }), 124 }; 125 } 126 127 const relayUrl = `${RELAY_BASE}?topicID=${encodeURIComponent(topicID)}`; 128 129 const payload = { 130 command: command || "displayPlaylist", 131 request, 132 }; 133 134 try { 135 const response = await fetch(relayUrl, { 136 method: "POST", 137 headers: { 138 "Content-Type": "application/json", 139 "API-KEY": apiKey, 140 }, 141 body: JSON.stringify(payload), 142 }); 143 144 const responseText = await response.text(); 145 let responseData; 146 try { 147 responseData = JSON.parse(responseText); 148 } catch { 149 responseData = { raw: responseText }; 150 } 151 152 if (!response.ok) { 153 // Check for Cloudflare worker errors (relay may be down) 154 if (response.status === 404 || responseText.includes("error code: 1042")) { 155 return { 156 statusCode: 503, 157 headers: corsHeaders(), 158 body: JSON.stringify({ 159 success: false, 160 error: "FF1 cloud relay appears to be unavailable", 161 hint: "The Feral File relay service may be down. Try using direct mode with deviceIp, or use the local tunnel (ac-ff1 tunnel).", 162 details: responseData, 163 }), 164 }; 165 } 166 167 return { 168 statusCode: response.status, 169 headers: corsHeaders(), 170 body: JSON.stringify({ 171 success: false, 172 error: `FF1 relay error: ${response.status}`, 173 details: responseData, 174 }), 175 }; 176 } 177 178 return { 179 statusCode: 200, 180 headers: corsHeaders(), 181 body: JSON.stringify({ success: true, response: responseData, mode: "relay" }), 182 }; 183 } catch (err) { 184 return { 185 statusCode: 500, 186 headers: corsHeaders(), 187 body: JSON.stringify({ success: false, error: err.message }), 188 }; 189 } 190}