commit
1ca61114ef
@ -1,10 +1,9 @@
|
||||
{
|
||||
"projectName": "homepage",
|
||||
"projectOwner": "benphelps",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": ["README.md"],
|
||||
"imageSize": 50,
|
||||
"contributorsSortAlphabetically": true,
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"contributors": []
|
||||
}
|
||||
|
@ -0,0 +1,323 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"readarr": {
|
||||
"queued": "Queued",
|
||||
"books": "Books",
|
||||
"wanted": "Wanted"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"traefik": {
|
||||
"services": "Services",
|
||||
"middleware": "Middleware",
|
||||
"routers": "Routers"
|
||||
},
|
||||
"mastodon": {
|
||||
"domain_count": "Domains",
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts"
|
||||
},
|
||||
"authentik": {
|
||||
"users": "Users",
|
||||
"loginsLast24H": "Logins (24h)",
|
||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"cpu": "CPU",
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"unifi": {
|
||||
"users": "Users",
|
||||
"uptime": "System Uptime",
|
||||
"days": "Days",
|
||||
"wan": "WAN",
|
||||
"lan": "LAN",
|
||||
"wlan": "WLAN",
|
||||
"devices": "Devices",
|
||||
"lan_devices": "LAN Devices",
|
||||
"wlan_devices": "WLAN Devices",
|
||||
"lan_users": "LAN Users",
|
||||
"wlan_users": "WLAN Users",
|
||||
"up": "UP",
|
||||
"down": "DOWN",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"changedetectionio": {
|
||||
"totalObserved": "Total Observed",
|
||||
"diffsDetected": "Diffs Detected"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"plex": {
|
||||
"streams": "Active Streams",
|
||||
"movies": "Movies",
|
||||
"tv": "TV Shows"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"missing": "Missing",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"navidrome": {
|
||||
"nothing_streaming": "No Active Streams",
|
||||
"please_wait": "Please Wait"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
},
|
||||
"proxmox": {
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"lxc": "LXC",
|
||||
"vms": "VMs"
|
||||
},
|
||||
"glances": {
|
||||
"cpu": "CPU",
|
||||
"mem": "MEM",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"quicklaunch": {
|
||||
"bookmark": "Bookmark",
|
||||
"service": "Service"
|
||||
},
|
||||
"wmo": {
|
||||
"0-day": "Sunny",
|
||||
"0-night": "Clear",
|
||||
"1-day": "Mainly Sunny",
|
||||
"1-night": "Mainly Clear",
|
||||
"2-day": "Partly Cloudy",
|
||||
"2-night": "Partly Cloudy",
|
||||
"3-day": "Cloudy",
|
||||
"3-night": "Cloudy",
|
||||
"45-day": "Foggy",
|
||||
"45-night": "Foggy",
|
||||
"48-day": "Foggy",
|
||||
"48-night": "Foggy",
|
||||
"51-day": "Light Drizzle",
|
||||
"51-night": "Light Drizzle",
|
||||
"53-day": "Drizzle",
|
||||
"53-night": "Drizzle",
|
||||
"55-day": "Heavy Drizzle",
|
||||
"55-night": "Heavy Drizzle",
|
||||
"56-day": "Light Freezing Drizzle",
|
||||
"56-night": "Light Freezing Drizzle",
|
||||
"57-day": "Freezing Drizzle",
|
||||
"57-night": "Freezing Drizzle",
|
||||
"61-day": "Light Rain",
|
||||
"61-night": "Light Rain",
|
||||
"63-day": "Rain",
|
||||
"63-night": "Rain",
|
||||
"65-day": "Heavy Rain",
|
||||
"65-night": "Heavy Rain",
|
||||
"66-day": "Freezing Rain",
|
||||
"66-night": "Freezing Rain",
|
||||
"67-day": "Freezing Rain",
|
||||
"67-night": "Freezing Rain",
|
||||
"71-day": "Light Snow",
|
||||
"71-night": "Light Snow",
|
||||
"73-day": "Snow",
|
||||
"73-night": "Snow",
|
||||
"75-day": "Heavy Snow",
|
||||
"75-night": "Heavy Snow",
|
||||
"77-day": "Snow Grains",
|
||||
"77-night": "Snow Grains",
|
||||
"80-day": "Light Showers",
|
||||
"80-night": "Light Showers",
|
||||
"81-day": "Showers",
|
||||
"81-night": "Showers",
|
||||
"82-day": "Heavy Showers",
|
||||
"82-night": "Heavy Showers",
|
||||
"85-day": "Snow Showers",
|
||||
"85-night": "Snow Showers",
|
||||
"86-day": "Snow Showers",
|
||||
"86-night": "Snow Showers",
|
||||
"95-day": "Thunderstorm",
|
||||
"95-night": "Thunderstorm",
|
||||
"96-day": "Thunderstorm With Hail",
|
||||
"96-night": "Thunderstorm With Hail",
|
||||
"99-day": "Thunderstorm With Hail",
|
||||
"99-night": "Thunderstorm With Hail"
|
||||
},
|
||||
"homebridge": {
|
||||
"available_update": "System",
|
||||
"updates": "Updates",
|
||||
"update_available": "Update Available",
|
||||
"up_to_date": "Up to Date",
|
||||
"child_bridges": "Child Bridges",
|
||||
"child_bridges_status": "{{ok}}/{{total}}"
|
||||
},
|
||||
"watchtower": {
|
||||
"containers_scanned": "Scanned",
|
||||
"containers_updated": "Updated",
|
||||
"containers_failed": "Failed"
|
||||
},
|
||||
"autobrr": {
|
||||
"approvedPushes": "Approved",
|
||||
"rejectedPushes": "Rejected",
|
||||
"filters": "Filters",
|
||||
"indexers": "Indexers"
|
||||
},
|
||||
"tubearchivist": {
|
||||
"downloads": "Queue",
|
||||
"videos": "Videos",
|
||||
"channels": "Channels",
|
||||
"playlists": "Playlists"
|
||||
},
|
||||
"truenas": {
|
||||
"load": "System Load",
|
||||
"uptime": "Uptime",
|
||||
"time": "{{value, number(style: unit; unitDisplay: long;)}}",
|
||||
"alerts": "Alerts"
|
||||
},
|
||||
"pyload": {
|
||||
"speed": "Speed",
|
||||
"active": "Active",
|
||||
"queue": "Queue",
|
||||
"total": "Total"
|
||||
},
|
||||
"gluetun": {
|
||||
"public_ip": "Public IP",
|
||||
"region": "Region",
|
||||
"country": "Country"
|
||||
}
|
||||
}
|
@ -0,0 +1,323 @@
|
||||
{
|
||||
"resources": {
|
||||
"cpu": "CPU",
|
||||
"total": "Jumlah",
|
||||
"free": "Bebas",
|
||||
"used": "Telah diguna",
|
||||
"load": "Beban"
|
||||
},
|
||||
"unifi": {
|
||||
"uptime": "Masa Operasi Sistem",
|
||||
"users": "Pengguna",
|
||||
"days": "Hari",
|
||||
"wan": "WAN",
|
||||
"lan": "LAN",
|
||||
"wlan": "WLAN",
|
||||
"devices": "Peranti",
|
||||
"lan_devices": "Peranti LAN",
|
||||
"wlan_devices": "Peranti WLAN",
|
||||
"lan_users": "Pengguna LAN",
|
||||
"wlan_users": "Pengguna WLAN",
|
||||
"up": "HIDUP",
|
||||
"down": "MATI",
|
||||
"wait": "Sila tunggu"
|
||||
},
|
||||
"lidarr": {
|
||||
"queued": "Dibaris Gilir",
|
||||
"albums": "Album",
|
||||
"wanted": "Mahu"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Mahu",
|
||||
"queued": "Dibaris Gilir",
|
||||
"books": "Buku"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Tertangguh",
|
||||
"approved": "Lulus",
|
||||
"available": "Sudah Ada"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"30days": "30 Hari",
|
||||
"configure": "Konfigurasikan satu atau lebih matawang crypto untuk dipantau",
|
||||
"1hour": "1 Jam",
|
||||
"1day": "1 Hari",
|
||||
"7days": "7 Hari"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplikasi",
|
||||
"clients": "Klien",
|
||||
"messages": "Mesej"
|
||||
},
|
||||
"proxmox": {
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"lxc": "LXC",
|
||||
"vms": "Mesin Maya"
|
||||
},
|
||||
"glances": {
|
||||
"cpu": "CPU",
|
||||
"mem": "MEM",
|
||||
"wait": "Sila tunggu"
|
||||
},
|
||||
"quicklaunch": {
|
||||
"bookmark": "Tandabuku",
|
||||
"service": "Servis"
|
||||
},
|
||||
"wmo": {
|
||||
"0-day": "Terik",
|
||||
"0-night": "Cerah",
|
||||
"1-day": "Sebahagian Besar Terik",
|
||||
"1-night": "Sebahagian Besar Cerah",
|
||||
"63-day": "Hujan",
|
||||
"63-night": "Hujan",
|
||||
"2-day": "Sebahagian Mendung",
|
||||
"2-night": "Sebahagian Mendung",
|
||||
"3-day": "Mendung",
|
||||
"3-night": "Mendung",
|
||||
"45-day": "Berkabus",
|
||||
"45-night": "Berkabus",
|
||||
"48-day": "Berkabus",
|
||||
"48-night": "Berkabus",
|
||||
"51-day": "Gerimis",
|
||||
"51-night": "Gerimis",
|
||||
"53-day": "Renyai",
|
||||
"53-night": "Renyai",
|
||||
"55-day": "Renyai Kuat",
|
||||
"55-night": "Renyai Kuat",
|
||||
"56-day": "Gerimis Sejuk Ringan",
|
||||
"56-night": "Gerimis Sejuk Ringan",
|
||||
"57-day": "Gerimis Sejuk",
|
||||
"57-night": "Gerimis Sejuk",
|
||||
"61-day": "Hujan Renyai",
|
||||
"61-night": "Hujan Renyai",
|
||||
"65-day": "Hujan Lebat",
|
||||
"65-night": "Hujan Lebat",
|
||||
"66-day": "Hujan Sejuk",
|
||||
"66-night": "Hujan Sejuk",
|
||||
"67-day": "Hujan Sejuk",
|
||||
"67-night": "Hujan Sejuk",
|
||||
"71-day": "Salji Ringan",
|
||||
"71-night": "Salji Ringan",
|
||||
"73-day": "Salji",
|
||||
"73-night": "Salji",
|
||||
"75-day": "Salji Lebat",
|
||||
"75-night": "Salji Lebat",
|
||||
"81-day": "Rintik",
|
||||
"77-day": "Butiran Salji",
|
||||
"77-night": "Butiran Salji",
|
||||
"80-day": "Rintik Ringan",
|
||||
"80-night": "Rintik Ringan",
|
||||
"81-night": "Rintik",
|
||||
"82-day": "Rintik Lebat",
|
||||
"82-night": "Rintik Lebat",
|
||||
"85-day": "Rintik Salji",
|
||||
"85-night": "Rintik Salji",
|
||||
"86-day": "Rintik Salji",
|
||||
"86-night": "Rintik Salji",
|
||||
"95-day": "Ribut",
|
||||
"95-night": "Ribut",
|
||||
"96-day": "Ribut Hujan Batu",
|
||||
"96-night": "Ribut Hujan Batu",
|
||||
"99-day": "Ribut Hujan Batu",
|
||||
"99-night": "Ribut Hujan Batu"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Jenis Widget Hilang: {{type}}",
|
||||
"api_error": "Masalah API",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Lokasi Sekarang",
|
||||
"allow": "Klik untuk benarkan",
|
||||
"updating": "Mengemas kini",
|
||||
"wait": "Sila tunggu"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Carian…"
|
||||
},
|
||||
"nzbget": {
|
||||
"remaining": "Baki",
|
||||
"downloaded": "Telah Muat Turun",
|
||||
"rate": "Kadar"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Luar talian"
|
||||
},
|
||||
"changedetectionio": {
|
||||
"totalObserved": "Jumlah Diperhatikan",
|
||||
"diffsDetected": "Perbezaan Dikesan"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Sedang dimainkan",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Kadar bit",
|
||||
"no_active": "Tiada Strim Aktif"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Sedang Dimainkan",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Kadar bit",
|
||||
"no_active": "Tiada Strim Aktif"
|
||||
},
|
||||
"plex": {
|
||||
"streams": "Strim Aktif",
|
||||
"movies": "Filem",
|
||||
"tv": "Rancangan TV"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Kadar",
|
||||
"queue": "Barisan",
|
||||
"timeleft": "Masa Tinggal"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktif",
|
||||
"upload": "Muat Naik",
|
||||
"download": "Muat Turun"
|
||||
},
|
||||
"transmission": {
|
||||
"leech": "Leech",
|
||||
"download": "Muat Turun",
|
||||
"upload": "Muat Naik",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Muat Turun",
|
||||
"upload": "Muat Naik",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Mahu",
|
||||
"queued": "Dibaris Gilir",
|
||||
"series": "Bersiri"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Mahu",
|
||||
"missing": "Hilang",
|
||||
"queued": "Dibaris Gilir",
|
||||
"movies": "Filem"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Episod Yang Hilang",
|
||||
"missingMovies": "Filem Yang Hilang"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Tertunda",
|
||||
"approved": "Lulus",
|
||||
"available": "Sudah Ada"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Tertangguh",
|
||||
"approved": "Lulus",
|
||||
"available": "Sudah Ada"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Permintaan",
|
||||
"blocked": "Disekat",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Permintaan",
|
||||
"blocked": "Disekat",
|
||||
"filtered": "Ditapis",
|
||||
"latency": "Kependaman"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Muat Naik",
|
||||
"download": "Muat Turun",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Sedang Berjalan",
|
||||
"stopped": "Terhenti",
|
||||
"total": "Jumlah"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Router",
|
||||
"services": "Servis",
|
||||
"middleware": "Perisian Tengah"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Didayakan",
|
||||
"disabled": "Dinyahdayakan",
|
||||
"total": "Jumlah"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Pengindeks",
|
||||
"numberOfGrabs": "Capai",
|
||||
"numberOfQueries": "Permintaan",
|
||||
"numberOfFailGrabs": "Capai Yang Ggagal",
|
||||
"numberOfFailQueries": "Permintaan Yang Gagal"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Telah Dikonfigurasi",
|
||||
"errored": "Telah Tersalah"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sesi",
|
||||
"numConnections": "Penyambungan",
|
||||
"dataRelayed": "Disalurkan",
|
||||
"transferRate": "Kadar"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Pengguna",
|
||||
"status_count": "Pos",
|
||||
"domain_count": "Domain"
|
||||
},
|
||||
"authentik": {
|
||||
"users": "Pengguna",
|
||||
"loginsLast24H": "Logmasuk (24j)",
|
||||
"failedLoginsLast24H": "Logmasuk Gagal (24j)"
|
||||
},
|
||||
"homebridge": {
|
||||
"child_bridges_status": "{{ok}}/{{total}}",
|
||||
"available_update": "Sistem",
|
||||
"updates": "Kemaskini",
|
||||
"update_available": "Kemaskini Tersedia",
|
||||
"up_to_date": "Terkemaskini",
|
||||
"child_bridges": "Jambatan Anak"
|
||||
},
|
||||
"watchtower": {
|
||||
"containers_scanned": "Terimbas",
|
||||
"containers_updated": "Dikemaskini",
|
||||
"containers_failed": "Gagal"
|
||||
},
|
||||
"autobrr": {
|
||||
"approvedPushes": "Lulus",
|
||||
"rejectedPushes": "Ditolak",
|
||||
"filters": "Tapisan",
|
||||
"indexers": "Pengindeks"
|
||||
},
|
||||
"tubearchivist": {
|
||||
"downloads": "Baris Gilir",
|
||||
"videos": "Video",
|
||||
"channels": "Saluran",
|
||||
"playlists": "Senarai Siar"
|
||||
},
|
||||
"truenas": {
|
||||
"load": "Beban Sistem",
|
||||
"uptime": "Masa Hidup",
|
||||
"alerts": "Amaran",
|
||||
"time": "{{value, number(style: unit; unitDisplay: long;)}}"
|
||||
},
|
||||
"navidrome": {
|
||||
"nothing_streaming": "Tiada Strim Aktif",
|
||||
"please_wait": "Sila tunggu"
|
||||
},
|
||||
"pyload": {
|
||||
"speed": "Kelajuan",
|
||||
"active": "Aktif",
|
||||
"queue": "Baris Gilir",
|
||||
"total": "Jumlah"
|
||||
},
|
||||
"gluetun": {
|
||||
"public_ip": "Public IP",
|
||||
"region": "Region",
|
||||
"country": "Country"
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: gluetunData, error: gluetunError } = useWidgetAPI(widget, "ip");
|
||||
|
||||
if (gluetunError) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!gluetunData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gluetun.public_ip" />
|
||||
<Block label="gluetun.region" />
|
||||
<Block label="gluetun.country" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gluetun.public_ip" value={gluetunData.public_ip} />
|
||||
<Block label="gluetun.region" value={gluetunData.region} />
|
||||
<Block label="gluetun.country" value={gluetunData.country} />
|
||||
</Container>
|
||||
);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/v1/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
ip: {
|
||||
endpoint: "publicip/ip",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
@ -0,0 +1,56 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
function SinglePlayingEntry({ entry }) {
|
||||
const { username, artist, title, album } = entry;
|
||||
let fullTitle = title;
|
||||
if (artist) fullTitle = `${artist} - ${title}`;
|
||||
if (album) fullTitle += ` — ${album}`;
|
||||
if (username) fullTitle += ` (${username})`;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{fullTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: navidromeData, error: navidromeError } = useWidgetAPI(widget, "getNowPlaying");
|
||||
|
||||
if (navidromeError || navidromeData?.error || navidromeData?.["subsonic-response"]?.error) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!navidromeData) {
|
||||
return (
|
||||
<SinglePlayingEntry entry={{ title: t("navidrome.please_wait") }} />
|
||||
);
|
||||
}
|
||||
|
||||
const { nowPlaying } = navidromeData["subsonic-response"];
|
||||
if (!nowPlaying.entry) {
|
||||
// nothing playing
|
||||
return (
|
||||
<SinglePlayingEntry entry={{ title: t("navidrome.nothing_streaming") }} />
|
||||
);
|
||||
}
|
||||
|
||||
const nowPlayingEntries = Object.values(nowPlaying.entry);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{nowPlayingEntries.map((entry) => (
|
||||
<SinglePlayingEntry key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/rest/{endpoint}?u={user}&t={token}&s={salt}&v=1.16.1&c=homepage&f=json",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"getNowPlaying": {
|
||||
endpoint: "getNowPlaying",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
@ -0,0 +1,35 @@
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { data: pyloadData, error: pyloadError } = useWidgetAPI(widget, "status");
|
||||
|
||||
if (pyloadError || pyloadData?.error) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!pyloadData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="pyload.speed" />
|
||||
<Block label="pyload.active" />
|
||||
<Block label="pyload.queue" />
|
||||
<Block label="pyload.total" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="pyload.speed" value={t("common.bitrate", { value: pyloadData.speed })} />
|
||||
<Block label="pyload.active" value={t("common.number", { value: pyloadData.active })} />
|
||||
<Block label="pyload.queue" value={t("common.number", { value: pyloadData.queue })} />
|
||||
<Block label="pyload.total" value={t("common.number", { value: pyloadData.total })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import cache from 'memory-cache';
|
||||
|
||||
import getServiceWidget from 'utils/config/service-helpers';
|
||||
import { formatApiCall } from 'utils/proxy/api-helpers';
|
||||
import widgets from 'widgets/widgets';
|
||||
import createLogger from 'utils/logger';
|
||||
import { httpProxy } from 'utils/proxy/http';
|
||||
|
||||
const proxyName = 'pyloadProxyHandler';
|
||||
const logger = createLogger(proxyName);
|
||||
const sessionCacheKey = `${proxyName}__sessionId`;
|
||||
const isNgCacheKey = `${proxyName}__isNg`;
|
||||
|
||||
async function fetchFromPyloadAPI(url, sessionId, params) {
|
||||
const options = {
|
||||
body: params
|
||||
? Object.keys(params)
|
||||
.map((prop) => `${prop}=${params[prop]}`)
|
||||
.join('&')
|
||||
: `session=${sessionId}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
};
|
||||
|
||||
// see https://github.com/benphelps/homepage/issues/517
|
||||
const isNg = cache.get(isNgCacheKey);
|
||||
if (isNg && !params) {
|
||||
delete options.body;
|
||||
options.headers.Cookie = cache.get(sessionCacheKey);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [status, contentType, data, responseHeaders] = await httpProxy(url, options);
|
||||
let returnData;
|
||||
try {
|
||||
returnData = JSON.parse(Buffer.from(data).toString());
|
||||
} catch(e) {
|
||||
logger.error(`Error logging into pyload API: ${JSON.stringify(data)}`);
|
||||
returnData = data;
|
||||
}
|
||||
return [status, returnData, responseHeaders];
|
||||
}
|
||||
|
||||
async function login(loginUrl, username, password = '') {
|
||||
const [status, sessionId, responseHeaders] = await fetchFromPyloadAPI(loginUrl, null, { username, password });
|
||||
|
||||
// this API actually returns status 200 even on login failure
|
||||
if (status !== 200 || sessionId === false) {
|
||||
logger.error(`HTTP ${status} logging into Pyload API, returned: ${JSON.stringify(sessionId)}`);
|
||||
} else if (responseHeaders['set-cookie']?.join().includes('pyload_session')) {
|
||||
// Support pyload-ng, see https://github.com/benphelps/homepage/issues/517
|
||||
cache.put(isNgCacheKey, true);
|
||||
const sessionCookie = responseHeaders['set-cookie'][0];
|
||||
cache.put(sessionCacheKey, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h
|
||||
} else {
|
||||
cache.put(sessionCacheKey, sessionId);
|
||||
}
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
export default async function pyloadProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
try {
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
const loginUrl = `${widget.url}/api/login`;
|
||||
|
||||
let sessionId = cache.get(sessionCacheKey) ?? await login(loginUrl, widget.username, widget.password);
|
||||
let [status, data] = await fetchFromPyloadAPI(url, sessionId);
|
||||
|
||||
if (status === 403 || status === 401) {
|
||||
logger.info('Failed to retrieve data from Pyload API, trying to login again...');
|
||||
cache.del(sessionCacheKey);
|
||||
sessionId = await login(loginUrl, widget.username, widget.password);
|
||||
[status, data] = await fetchFromPyloadAPI(url, sessionId);
|
||||
}
|
||||
|
||||
if (data?.error || status !== 200) {
|
||||
try {
|
||||
return res.status(status).send(Buffer.from(data).toString());
|
||||
} catch (e) {
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(data);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return res.status(500).send(e.toString());
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: 'Invalid proxy service type' });
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import pyloadProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: pyloadProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"status": {
|
||||
endpoint: "statusServer",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default widget;
|
Loading…
Reference in new issue