Compare commits

5 Commits

Author SHA1 Message Date
Jack Dallas
2e75f89615 Update README.md 2022-06-04 21:29:11 +01:00
Jack Dallas
3769dcb030 Set default Docker directories 2022-06-04 21:29:11 +01:00
Jack Dallas
f1b43bc52c Update docker default config and log location 2022-06-04 21:08:23 +01:00
Jack Dallas
ba18439aa5 Add Config API Service & Refactor Config Implementation 2022-06-04 21:08:23 +01:00
Jack Dallas
3cd946e2ca Add Config page to UI 2022-06-04 21:08:23 +01:00
13 changed files with 1831 additions and 1746 deletions

View File

@@ -5,11 +5,16 @@ RUN apt update && \
apt install ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
ENV PREMIUMIZEARR_CONFIG_DIR_PATH=/data
ENV PREMIUMIZEARR_LOGGING_DIR_PATH=/data
EXPOSE 8182
WORKDIR /opt/app/
COPY premiumizearrd /opt/app/
COPY build/static /opt/app/static
EXPOSE 8182
ENTRYPOINT [ "/opt/app/premiumizearrd" ]

View File

@@ -39,38 +39,19 @@ sudo dpkg -i premiumizearr_x.x.x.x_linux_amd64.deb
[Docker images are listed here](https://github.com/jackdallas/Premiumizearr/pkgs/container/premiumizearr)
`docker run ghcr.io/jackdallas/premiumizearr:latest /host/config.yaml:/opt/app/config.yaml -v /host/downloads:/downloads -v /host/blackhole:/blackhole`
`docker run -p 8182:8182 -v /host/data/path:/data -v /host/downloads/path:/downloads -v /host/blackhole/path:/blackhole ghcr.io/jackdallas/premiumizearr:latest`
You'll need to then point your config at `/blackhole` and `/downloads` (config explained more below)
> Note: The /data mount is where the `config.yaml` and log files are kept
## Setup
### Premiumizearrd
Edit the config file at `/opt/premiumizearrd/config.yml`
Running for the first time the server will start on http://0.0.0.0:8182
`Arrs:` A list of *Arr clients you wish to connect to in the format`
If you already use this binding you can edit them in the `config.yaml`
```
- Name: "Sonarr 1"
URL: http://127.0.0.1:8989
APIKey: xxxxxxx
Type: Sonarr
```
Note: Type is either `Sonarr` or `Radarr` with a capital letter
`PremiumizemeAPIKey` API key for your [premiumize.me](https://www.premiumize.me) account
`BlackholeDirectory` Path to Directory the Arr's will put magnet/torrent/nzb files in
`DownloadsDirectory` Path for Premiumizearr to download media files to, that the Arr's watch for new media
`UnzipDirectory` Path for Premiumizearr to use to temporarily unzip downloads before moving, leave blank and a path in temp will me made
`bindIP` IP the web server binds to
`bindPort` Port the web server binds to
> Note: Currently most changes in the config ui will not be used until a restart is complete
### Sonarr/Radarr

View File

@@ -2,6 +2,7 @@ package main
import (
"flag"
"path"
"time"
"github.com/jackdallas/premiumizearr/internal/arr"
@@ -21,10 +22,12 @@ func main() {
//Flags
var logLevel string
var configFile string
var loggingDirectory string
//Parse flags
flag.StringVar(&logLevel, "log", utils.EnvOrDefault("PREMIUMIZEARR_LOG_LEVEL", "info"), "Logging level: \n \tinfo,debug,trace")
flag.StringVar(&configFile, "config", utils.EnvOrDefault("PREMIUMIZEARR_CONFIG_PATH", ""), "Config file path")
flag.StringVar(&configFile, "config", utils.EnvOrDefault("PREMIUMIZEARR_CONFIG_DIR_PATH", "./"), "The directory the config.yml is located in")
flag.StringVar(&loggingDirectory, "logging-dir", utils.EnvOrDefault("PREMIUMIZEARR_LOGGING_DIR_PATH", "./"), "The directory logs are to be written to")
flag.Parse()
lvl, err := log.ParseLevel(logLevel)
@@ -35,7 +38,7 @@ func main() {
log.SetLevel(lvl)
hook, err := lumberjackrus.NewHook(
&lumberjackrus.LogFile{
Filename: "/opt/premiumizearrd/premiumizearr.general.log",
Filename: path.Join(loggingDirectory, "premiumizearr.general.log"),
MaxSize: 100,
MaxBackups: 1,
MaxAge: 1,
@@ -46,7 +49,7 @@ func main() {
&log.TextFormatter{},
&lumberjackrus.LogFileOpts{
log.InfoLevel: &lumberjackrus.LogFile{
Filename: "/opt/premiumizearrd/premiumizearr.info.log",
Filename: path.Join(loggingDirectory, "premiumizearr.info.log"),
MaxSize: 100,
MaxBackups: 1,
MaxAge: 1,
@@ -54,7 +57,7 @@ func main() {
LocalTime: false,
},
log.ErrorLevel: &lumberjackrus.LogFile{
Filename: "/opt/premiumizearrd/premiumizearr.error.log",
Filename: path.Join(loggingDirectory, "premiumizearr.error.log"),
MaxSize: 100, // optional
MaxBackups: 1, // optional
MaxAge: 1, // optional
@@ -75,12 +78,25 @@ func main() {
config, err := config.LoadOrCreateConfig(configFile)
// Override config data directories if running in docker
if utils.IsRunningInDockerContainer() {
if config.BlackholeDirectory == "" {
config.BlackholeDirectory = "/blackhole"
}
if config.DownloadsDirectory == "" {
config.DownloadsDirectory = "/downloads"
}
if config.UnzipDirectory == "" {
config.UnzipDirectory = "/unzip"
}
}
if err != nil {
panic(err)
}
if config.PremiumizemeAPIKey == "" {
panic("premiumizearr API Key is empty")
log.Warn("Premiumizeme API key not set, application will not work until it's set")
}
// Initialisation

View File

@@ -3,7 +3,9 @@ package config
import (
"errors"
"io/ioutil"
"log"
log "github.com/sirupsen/logrus"
"os"
"path"
@@ -31,6 +33,8 @@ type ArrConfig struct {
}
type Config struct {
altConfigLocation string
PremiumizemeAPIKey string `yaml:"PremiumizemeAPIKey"`
Arrs []ArrConfig `yaml:"Arrs"`
@@ -48,9 +52,9 @@ type Config struct {
SimultaneousDownloads int `yaml:"SimultaneousDownloads"`
}
func loadConfigFromDisk() (Config, error) {
func loadConfigFromDisk(altConfigLocation string) (Config, error) {
var config Config
file, err := ioutil.ReadFile("config.yaml")
file, err := ioutil.ReadFile(path.Join(altConfigLocation, "config.yaml"))
if err != nil {
return config, ErrFailedToFindConfigFile
@@ -63,13 +67,25 @@ func loadConfigFromDisk() (Config, error) {
config = versionUpdateConfig(config)
data, err := yaml.Marshal(config)
config.Save()
config.altConfigLocation = altConfigLocation
return config, nil
}
func (c *Config) Save() bool {
data, err := yaml.Marshal(*c)
if err == nil {
//Save config to disk to add missing fields
ioutil.WriteFile("config.yaml", data, 0644)
log.Tracef("Saving config to %s", path.Join(c.altConfigLocation, "config.yaml"))
err = ioutil.WriteFile(path.Join(c.altConfigLocation, "config.yaml"), data, 0644)
if err != nil {
log.Errorf("Failed to save config file: %+v", err)
return false
}
}
return config, nil
log.Trace("Config saved")
return true
}
func versionUpdateConfig(config Config) Config {
@@ -81,12 +97,12 @@ func versionUpdateConfig(config Config) Config {
return config
}
func createDefaultConfig() error {
config := Config{
func defaultConfig(altConfigLocation string) Config {
return Config{
PremiumizemeAPIKey: "xxxxxxxxx",
Arrs: []ArrConfig{
{URL: "http://localhost:8989", APIKey: "xxxxxxxxx", Type: Sonarr},
{URL: "http://localhost:7878", APIKey: "xxxxxxxxx", Type: Radarr},
{Name: "Sonarr", URL: "http://localhost:8989", APIKey: "xxxxxxxxx", Type: Sonarr},
{Name: "Radarr", URL: "http://localhost:7878", APIKey: "xxxxxxxxx", Type: Radarr},
},
BlackholeDirectory: "",
DownloadsDirectory: "",
@@ -96,35 +112,15 @@ func createDefaultConfig() error {
WebRoot: "",
SimultaneousDownloads: 5,
}
file, err := yaml.Marshal(config)
if err != nil {
return err
}
err = ioutil.WriteFile("config.yaml", file, 0644)
if err != nil {
return err
}
return nil
}
func LoadOrCreateConfig(altConfigLocation string) (Config, error) {
if altConfigLocation != "" {
if _, err := ioutil.ReadFile(altConfigLocation); err != nil {
log.Panicf("Failed to find config file at %s Error: %+v", altConfigLocation, err)
}
}
config, err := loadConfigFromDisk()
config, err := loadConfigFromDisk(altConfigLocation)
if err != nil {
if err == ErrFailedToFindConfigFile {
err = createDefaultConfig()
if err != nil {
return config, err
}
panic("Default config created, please fill it out")
config = defaultConfig(altConfigLocation)
log.Warn("No config file found, created default config file")
}
if err == ErrInvalidConfigFile {
return config, ErrInvalidConfigFile

View File

@@ -87,7 +87,8 @@ func GetDownloadsFolderIDFromPremiumizeme(premiumizemeClient *premiumizeme.Premi
folders, err := premiumizemeClient.GetFolders()
if err != nil {
log.Errorf("Error getting folders: %s", err)
log.Fatalf("Cannot read folders from premiumize.me, exiting!")
log.Errorf("Cannot read folders from premiumize.me, application will not run!")
return ""
}
const folderName = "arrDownloads"
@@ -102,7 +103,7 @@ func GetDownloadsFolderIDFromPremiumizeme(premiumizemeClient *premiumizeme.Premi
if len(downloadsFolderID) == 0 {
id, err := premiumizemeClient.CreateFolder(folderName)
if err != nil {
log.Fatalf("Cannot create downloads folder on premiumize.me, exiting! %+v", err)
log.Errorf("Cannot create downloads folder on premiumize.me, application will not run correctly! %+v", err)
}
downloadsFolderID = id
}
@@ -117,3 +118,16 @@ func EnvOrDefault(envName string, defaultValue string) string {
}
return envValue
}
func IsRunningInDockerContainer() bool {
// docker creates a .dockerenv file at the root
// of the directory tree inside the container.
// if this file exists then the viewer is running
// from inside a container so return true
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
return false
}

View File

@@ -0,0 +1,57 @@
package web_service
import (
"encoding/json"
"fmt"
"net/http"
"github.com/jackdallas/premiumizearr/internal/config"
)
type ConfigChangeResponse struct {
Succeeded bool `json:"succeeded"`
Status string `json:"status"`
}
func (s *server) ConfigHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
data, err := json.Marshal(s.config)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
case http.MethodPost:
var newConfig config.Config
err := json.NewDecoder(r.Body).Decode(&newConfig)
if err != nil {
EncodeAndWriteConfigChangeResponse(w, &ConfigChangeResponse{
Succeeded: false,
Status: fmt.Sprintf("Config failed to update %s", err.Error()),
})
return
}
s.config = &newConfig
s.config.Save()
EncodeAndWriteConfigChangeResponse(w, &ConfigChangeResponse{
Succeeded: true,
Status: "Config updated",
})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func EncodeAndWriteConfigChangeResponse(w http.ResponseWriter, resp *ConfigChangeResponse) {
data, err := json.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
}

View File

@@ -28,10 +28,12 @@ var indexBytes []byte
type server struct {
transferManager *service.TransferManagerService
directoryWatcherService *service.DirectoryWatcherService
config *config.Config
}
// http Router
func StartWebServer(transferManager *service.TransferManagerService, directoryWatcher *service.DirectoryWatcherService, config *config.Config) {
log.Info("Starting web server...")
tmpl, err := template.ParseFiles("./static/index.html")
if err != nil {
log.Fatal(err)
@@ -47,6 +49,7 @@ func StartWebServer(transferManager *service.TransferManagerService, directoryWa
s := server{
transferManager: transferManager,
directoryWatcherService: directoryWatcher,
config: config,
}
spa := spaHandler{
staticPath: "static",
@@ -59,27 +62,33 @@ func StartWebServer(transferManager *service.TransferManagerService, directoryWa
transferPath := "/api/transfers"
downloadsPath := "/api/downloads"
blackholePath := "/api/blackhole"
configPathBase := "/api/config"
if config.WebRoot != "" {
transferPath = path.Join(config.WebRoot, transferPath)
downloadsPath = path.Join(config.WebRoot, downloadsPath)
blackholePath = path.Join(config.WebRoot, blackholePath)
configPathBase = path.Join(config.WebRoot, configPathBase)
}
r.HandleFunc(transferPath, s.TransfersHandler)
r.HandleFunc(downloadsPath, s.DownloadsHandler)
r.HandleFunc(blackholePath, s.BlackholeHandler)
r.HandleFunc(configPathBase, s.ConfigHandler)
r.PathPrefix("/").Handler(spa)
address := fmt.Sprintf("%s:%s", config.BindIP, config.BindPort)
srv := &http.Server{
Handler: r,
Addr: fmt.Sprintf("%s:%s", config.BindIP, config.BindPort),
Addr: address,
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Infof("Web server started on %s", address)
srv.ListenAndServe()
}
@@ -173,7 +182,7 @@ func (s *server) BlackholeHandler(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}
// Shamlessly stolen from mux examples https://github.com/gorilla/mux#examples
// Shamelessly stolen from mux examples https://github.com/gorilla/mux#examples
type spaHandler struct {
staticPath string
indexPath string

2778
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,25 +2,25 @@
"name": "premiumizearr-ui",
"version": "0.0.1",
"devDependencies": {
"carbon-components-svelte": "^0.49.0",
"carbon-icons-svelte": "^10.38.0",
"carbon-preprocess-svelte": "^0.6.0",
"copy-webpack-plugin": "^9.1.0",
"cross-env": "^7.0.3",
"css-loader": "^5.0.1",
"esbuild-loader": "^2.16.0",
"mini-css-extract-plugin": "^1.3.4",
"svelte": "^3.31.2",
"carbon-components-svelte": "^0.64.0",
"carbon-icons-svelte": "^11.0.0",
"carbon-preprocess-svelte": "^0.9.0",
"copy-webpack-plugin": "^9.0.0",
"cross-env": "^7.0.0",
"css-loader": "^5.0.0",
"esbuild-loader": "^2.0.0",
"mini-css-extract-plugin": "^1.0.0",
"svelte": "^3.0.0",
"svelte-loader": "^3.0.0",
"webpack": "^5.16.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^4.7.3"
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0",
"webpack-dev-server": "^4.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"dev": "webpack serve --content-base public"
"dev": "webpack serve --static public"
},
"dependencies": {
"luxon": "^2.3.0"
"luxon": "^2.0.0"
}
}

View File

@@ -1,152 +1,31 @@
<script>
import APITable from "./components/APITable.svelte";
import "carbon-components-svelte/css/g100.css";
import { Grid, Row, Column } from "carbon-components-svelte";
import DateTime from "luxon";
let dlSpeed = 0;
let webRoot = window.location.href;
function parseDLSpeedFromMessage(m) {
if (m == "Loading..." || m == undefined) return 0;
if (m == "too many missing articles") return 0;
let speed = m.split(" ")[0];
speed = speed.replace(",", "");
let unit = m.split(" ")[1];
if (Number.isNaN(speed)) {
console.log("Speed is not a number: ", speed);
console.log("Message: ", message);
return 0;
}
if (unit === undefined || unit === null || unit == "") {
console.log("Unit undefined in : " + m);
return 0;
} else {
try {
unit = unit.toUpperCase();
} catch (error) {
return 0;
}
unit = unit.replace("/", "");
unit = unit.substring(0, 2);
switch (unit) {
case "KB":
return speed * 1024;
case "MB":
return speed * 1024 * 1024;
case "GB":
return speed * 1024 * 1024 * 1024;
default:
console.log("Unknown unit: " + unit + " in message '" + m + "'");
return 0;
}
}
}
function HumanReadableSpeed(bytes) {
if (bytes < 1024) {
return bytes + " B/s";
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + " KB/s";
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / 1024 / 1024).toFixed(2) + " MB/s";
} else {
return (bytes / 1024 / 1024 / 1024).toFixed(2) + " GB/s";
}
}
function dataToRows(data) {
let rows = [];
dlSpeed = 0;
if (!data) return rows;
for (let i = 0; i < data.length; i++) {
let d = data[i];
rows.push({
id: d.id,
name: d.name,
status: d.status,
progress: (d.progress * 100).toFixed(0) + "%",
message: d.message,
});
let speed = parseDLSpeedFromMessage(d.message);
if (!Number.isNaN(speed)) {
dlSpeed += speed;
} else {
console.error("Invalid speed: " + d.message);
}
}
return rows;
}
function downloadsToRows(downloads) {
let rows = [];
if (!downloads) return rows;
for (let i = 0; i < downloads.length; i++) {
let d = downloads[i];
rows.push({
Added: DateTime.fromMillis(d.added).toFormat('dd hh:mm:ss a'),
name: d.name,
progress: (d.progress * 100).toFixed(0) + "%",
});
}
}
</script>
<main>
<Grid fullWidth>
<Row>
<Column md={4} >
<h3>Blackhole</h3>
<APITable
headers={[
{ key: "id", value: "Pos" },
{ key: "name", value: "Name", sort: false },
]}
{webRoot}
APIpath="api/blackhole"
zebra={true}
totalName="In Queue: "
/>
</Column>
<Column md={4} >
<h3>Downloads</h3>
<APITable
headers={[
{ key: "added", value: "Added" },
{ key: "name", value: "Name" },
{ key: "progress", value: "Progress" },
{ key: "speed", value: "Speed" },
]}
updateTimeSeconds={2}
{webRoot}
APIpath="api/downloads"
zebra={true}
totalName="Downloading: "
/>
</Column>
</Row>
<Row>
<Column>
<h3>Transfers</h3>
<p>Download Speed: {HumanReadableSpeed(dlSpeed)}</p>
<APITable
headers={[
{ key: "name", value: "Name" },
{ key: "status", value: "Status" },
{ key: "progress", value: "Progress" },
{ key: "message", value: "Message", sort: false },
]}
{webRoot}
APIpath="api/transfers"
zebra={true}
{dataToRows}
/>
</Column>
</Row>
</Grid>
</main>
<script>
import "carbon-components-svelte/css/g100.css";
import {
Grid,
Row,
Column,
Tabs,
Tab,
TabContent,
} from "carbon-components-svelte";
import Config from "./pages/Config.svelte";
import Info from "./pages/Info.svelte";
</script>
<main>
<Grid fullWidth>
<Row>
<Column>
<Tabs>
<Tab label="Info" />
<Tab label="Config" />
<svelte:fragment slot="content">
<TabContent><Info /></TabContent>
<TabContent><Config /></TabContent>
</svelte:fragment>
</Tabs>
</Column>
</Row>
</Grid>
</main>

221
web/src/pages/Config.svelte Normal file
View File

@@ -0,0 +1,221 @@
<script>
import {
Row,
Column,
Button,
TextInput,
Modal,
FormGroup,
Dropdown,
} from "carbon-components-svelte";
import {
Save,
CheckmarkFilled,
AddFilled,
TrashCan,
} from "carbon-icons-svelte";
let webRoot = window.location.href;
let config = {
BlackholeDirectory: "",
DownloadsDirectory: "",
UnzipDirectory: "",
BindIP: "",
BindPort: "",
WebRoot: "",
SimultaneousDownloads: 0,
Arrs: [],
};
let inputDisabled = true;
let errorModal = false;
let errorMessage = "";
let saveIcon = Save;
function getConfig() {
inputDisabled = true;
fetch(webRoot + "api/config")
.then((response) => response.json())
.then((data) => {
config = data;
inputDisabled = false;
})
.catch((error) => {
console.error("Error: ", error);
});
}
function submit() {
inputDisabled = true;
fetch(webRoot + "api/config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(config),
})
.then((response) => response.json())
.then((data) => {
if (data.succeeded) {
saveIcon = CheckmarkFilled;
getConfig();
setTimeout(() => {
saveIcon = Save;
}, 1000);
} else {
errorMessage = data.status;
errorModal = true;
getConfig();
}
})
.catch((error) => {
console.error("Error: ", error);
errorModal = true;
errorMessage = error;
setTimeout(() => {
getConfig();
}, 1500);
});
}
function AddArr() {
config.Arrs.push({
Name: "New Arr",
URL: "http://localhost:1234",
APIKey: "xxxxxxxx",
Type: "Sonarr",
});
//Force re-paint
config.Arrs = [...config.Arrs];
}
function RemoveArr(index) {
console.log(index);
config.Arrs.splice(index, 1);
//Force re-paint
config.Arrs = [...config.Arrs];
}
getConfig();
</script>
<main>
<Row>
<Column>
<h4>*Arr Settings</h4>
<FormGroup>
{#if config.Arrs !== undefined}
{#each config.Arrs as arr, i}
<h5>- {arr.Name ? arr.Name : i}</h5>
<FormGroup>
<TextInput
labelText="Name"
bind:value={arr.Name}
disabled={inputDisabled}
/>
<TextInput
labelText="URL"
bind:value={arr.URL}
disabled={inputDisabled}
/>
<TextInput
labelText="APIKey"
bind:value={arr.APIKey}
disabled={inputDisabled}
/>
<Dropdown
titleText="Type"
selectedId={arr.Type}
on:select={(e) => {
config.Arrs[i].Type = e.detail.selectedId;
}}
items={[
{ id: "Sonarr", text: "Sonarr" },
{ id: "Radarr", text: "Radarr" },
]}
disabled={inputDisabled}
/>
<Button
style="margin-top: 10px;"
on:click={() => {
RemoveArr(i);
}}
kind="danger"
icon={TrashCan}
iconDescription="Delete Arr"
/>
</FormGroup>
{/each}
{/if}
</FormGroup>
<Button on:click={AddArr} disabled={inputDisabled} icon={AddFilled}>
Add Arr
</Button>
</Column>
<Column>
<h4>Directory Settings</h4>
<FormGroup>
<TextInput
disabled={inputDisabled}
labelText="Blackhole Directory"
bind:value={config.BlackholeDirectory}
/>
<TextInput
disabled={inputDisabled}
labelText="Download Directory"
bind:value={config.DownloadsDirectory}
/>
<TextInput
disabled={inputDisabled}
labelText="Unzip Directory"
bind:value={config.UnzipDirectory}
/>
</FormGroup>
<h4>Web Server Settings</h4>
<FormGroup>
<TextInput
disabled={inputDisabled}
labelText="Bind IP"
bind:value={config.BindIP}
/>
<TextInput
disabled={inputDisabled}
labelText="Bind Port"
bind:value={config.BindPort}
/>
<TextInput
disabled={inputDisabled}
labelText="Web Root"
bind:value={config.WebRoot}
/>
</FormGroup>
<h4>Download Settings</h4>
<FormGroup>
<TextInput
type="number"
disabled={inputDisabled}
labelText="Simultaneous Downloads"
bind:value={config.SimultaneousDownloads}
/>
</FormGroup>
<Button on:click={submit} icon={saveIcon} disabled={inputDisabled}
>Save</Button
>
</Column>
</Row>
</main>
<Modal
bind:open={errorModal}
on:open={errorModal}
passiveModal
modalHeading="Error Saving Config"
on:close={() => {
errorModal = false;
}}
>
<p>{errorMessage}</p>
</Modal>

149
web/src/pages/Info.svelte Normal file
View File

@@ -0,0 +1,149 @@
<script>
import APITable from "../components/APITable.svelte";
import { Row, Column } from "carbon-components-svelte";
import {DateTime} from "luxon";
let dlSpeed = 0;
let webRoot = window.location.href;
function parseDLSpeedFromMessage(m) {
if (m == "Loading..." || m == undefined) return 0;
if (m == "too many missing articles") return 0;
let speed = m.split(" ")[0];
speed = speed.replace(",", "");
let unit = m.split(" ")[1];
if (Number.isNaN(speed)) {
console.log("Speed is not a number: ", speed);
console.log("Message: ", message);
return 0;
}
if (unit === undefined || unit === null || unit == "") {
console.log("Unit undefined in : " + m);
return 0;
} else {
try {
unit = unit.toUpperCase();
} catch (error) {
return 0;
}
unit = unit.replace("/", "");
unit = unit.substring(0, 2);
switch (unit) {
case "KB":
return speed * 1024;
case "MB":
return speed * 1024 * 1024;
case "GB":
return speed * 1024 * 1024 * 1024;
default:
console.log("Unknown unit: " + unit + " in message '" + m + "'");
return 0;
}
}
}
function HumanReadableSpeed(bytes) {
if (bytes < 1024) {
return bytes + " B/s";
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + " KB/s";
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / 1024 / 1024).toFixed(2) + " MB/s";
} else {
return (bytes / 1024 / 1024 / 1024).toFixed(2) + " GB/s";
}
}
function dataToRows(data) {
let rows = [];
dlSpeed = 0;
if (!data) return rows;
for (let i = 0; i < data.length; i++) {
let d = data[i];
rows.push({
id: d.id,
name: d.name,
status: d.status,
progress: (d.progress * 100).toFixed(0) + "%",
message: d.message,
});
let speed = parseDLSpeedFromMessage(d.message);
if (!Number.isNaN(speed)) {
dlSpeed += speed;
} else {
console.error("Invalid speed: " + d.message);
}
}
return rows;
}
function downloadsToRows(downloads) {
let rows = [];
if (!downloads) return rows;
for (let i = 0; i < downloads.length; i++) {
let d = downloads[i];
rows.push({
Added: DateTime.fromMillis(d.added).toFormat('dd hh:mm:ss a'),
name: d.name,
progress: (d.progress * 100).toFixed(0) + "%",
});
}
}
</script>
<main>
<Row>
<Column md={4} >
<h3>Blackhole</h3>
<APITable
headers={[
{ key: "id", value: "Pos" },
{ key: "name", value: "Name", sort: false },
]}
{webRoot}
APIpath="api/blackhole"
zebra={true}
totalName="In Queue: "
/>
</Column>
<Column md={4} >
<h3>Downloads</h3>
<APITable
headers={[
{ key: "added", value: "Added" },
{ key: "name", value: "Name" },
{ key: "progress", value: "Progress" },
{ key: "speed", value: "Speed" },
]}
updateTimeSeconds={2}
{webRoot}
APIpath="api/downloads"
zebra={true}
totalName="Downloading: "
/>
</Column>
</Row>
<Row>
<Column>
<h3>Transfers</h3>
<p>Download Speed: {HumanReadableSpeed(dlSpeed)}</p>
<APITable
headers={[
{ key: "name", value: "Name" },
{ key: "status", value: "Status" },
{ key: "progress", value: "Progress" },
{ key: "message", value: "Message", sort: false },
]}
{webRoot}
APIpath="api/transfers"
zebra={true}
{dataToRows}
/>
</Column>
</Row>
</main>

View File

@@ -61,7 +61,7 @@ module.exports = {
devServer: {
hot: true,
proxy: {
'/api': 'https://yourinstance.com/api'
'/api': 'http://localhost:8182'
}
},
optimization: {