diff --git a/.golangci.yml b/.golangci.yml
index a0511f41..02b94faa 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -12,14 +12,20 @@ linters:
- tagliatelle
- cyclop
- testpackage
-run:
- timeout: 3m
issues:
max-issues-per-linter: 0
max-same-issues: 0
+ exclude-rules:
+ # Exclude some linters from testing files.
+ - linters:
+ - forbidigo
+ - lll
+ path: 'init/config/.*.\.go'
output:
sort-results: true
+run:
+ timeout: 3m
linters-settings:
ireturn:
@@ -41,4 +47,5 @@ linters-settings:
- github.com/radovskyb/watcher
- github.com/prometheus/client_golang/
- github.com/spf13/pflag
- - github.com/julienschmidt/httprouter
\ No newline at end of file
+ - github.com/julienschmidt/httprouter
+ - github.com/BurntSushi/toml
diff --git a/go.mod b/go.mod
index 49060e75..0fd5d18b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/Unpackerr/unpackerr
go 1.22
require (
+ github.com/BurntSushi/toml v1.4.0
github.com/fsnotify/fsnotify v1.7.0
github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d
github.com/getlantern/systray v1.2.2
@@ -20,11 +21,10 @@ require (
golift.io/starr v1.0.0
golift.io/version v0.0.2
golift.io/xtractr v0.2.3-0.20240710043203-2d7c8a38d931
-
+ gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/BurntSushi/toml v1.4.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
@@ -68,5 +68,4 @@ require (
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/init/config/README.md b/init/config/README.md
new file mode 100644
index 00000000..f8e082fa
--- /dev/null
+++ b/init/config/README.md
@@ -0,0 +1,3 @@
+- All params must have a default, even if it's `[]` or `''`.
+- Examples override params, but get commented out.
+- All list params are commented.
diff --git a/init/config/compose-builder.go b/init/config/compose-builder.go
new file mode 100644
index 00000000..f0c258d5
--- /dev/null
+++ b/init/config/compose-builder.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+const (
+ space = " "
+ composeHeader = `### Unpackerr docker-compose.yml Example
+### Please read this page for help using this example:
+### https://unpackerr.zip/docs/install/compose
+### Generator: https://notifiarr.com/unpackerr
+##################################################################
+services:
+
+ unpackerr:
+ image: golift/unpackerr
+ container_name: unpackerr
+ volumes:
+ # You need at least this one volume mapped so Unpackerr can find your files to extract.
+ # Make sure this matches your Starr apps; the folder mount (/downloads or /data) should be identical.
+ - /mnt/HostDownloads:/downloads
+ restart: always
+ # Get the user:group correct so unpackerr can read and write to your files.
+ user: ${PUID}:${PGID}
+ #user: 1000:100
+ # What you see below are defaults for this compose. You only need to modify things specific to your environment.
+ # Remove apps and feature configs you do not use or need.
+ # ie. Remove all lines that begin with UN_CMDHOOK, UN_WEBHOOK, UN_FOLDER, UN_WEBSERVER, and other apps you do not use.
+ environment:
+ - TZ=${TZ}`
+)
+
+func printCompose(config *Config) {
+ fmt.Println(composeHeader)
+
+ // Loop the 'Order' list.
+ for _, section := range config.Order {
+ // If Order contains a missing section, panic.
+ if config.Sections[section] == nil {
+ panic(section + ": in order, but missing from sections. This is a bug in conf-builder.yml.")
+ }
+
+ if config.Defs[section] == nil {
+ fmt.Print(config.Sections[section].makeCompose(config.Sections[section].Title, config.Prefix, false))
+ } else {
+ fmt.Print(config.Sections[section].makeComposeDefined(config.Prefix, config.Defs[section], config.DefOrder[section], false))
+ }
+ }
+}
+
+func (h *Header) makeCompose(title, prefix string, bare bool) string {
+ var buf bytes.Buffer
+
+ if len(h.Params) > 0 && bare {
+ buf.WriteString("## " + title + "\n")
+ } else if len(h.Params) > 0 {
+ buf.WriteString(space + " ## " + title + "\n")
+ }
+
+ pfx := space + " - "
+ if bare {
+ pfx = ""
+ }
+
+ for _, param := range h.Params {
+ if h.Kind == list {
+ buf.WriteString(param.Compose(pfx + prefix + h.Prefix + "0_"))
+ } else {
+ buf.WriteString(param.Compose(pfx + prefix + h.Prefix))
+ }
+ }
+
+ return buf.String()
+}
+
+func (h *Header) makeComposeDefined(prefix string, defs Defs, order []section, bare bool) string {
+ var buf bytes.Buffer
+
+ for _, section := range order {
+ newHeader := createDefinedSection(defs[section], h)
+ // Make a brand new section and print it.
+ buf.WriteString(newHeader.makeCompose(h.Title, prefix, bare))
+ }
+
+ return buf.String()
+}
+
+func (p *Param) Compose(prefix string) string {
+ val := p.Default
+ if p.Example != nil {
+ val = p.Example
+ }
+
+ switch p.Kind {
+ default:
+ return fmt.Sprint(prefix, p.EnvVar, "=", val, "\n")
+ case list:
+ var out string
+
+ for idx, sv := range val.([]any) { //nolint:forcetypeassert
+ out += fmt.Sprint(prefix, p.EnvVar, idx, "=", sv, "\n")
+ }
+
+ return out
+ case "conlist":
+ out := []string{}
+
+ for _, sv := range val.([]any) { //nolint:forcetypeassert
+ out = append(out, fmt.Sprint(sv))
+ }
+
+ return fmt.Sprint(prefix, p.EnvVar, "=", strings.Join(out, ","), "\n")
+ }
+}
diff --git a/init/config/conf-builder.go b/init/config/conf-builder.go
new file mode 100644
index 00000000..78e2aa08
--- /dev/null
+++ b/init/config/conf-builder.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+
+ "github.com/BurntSushi/toml"
+)
+
+func printConfFile(config *Config) {
+ // Loop the 'Order' list.
+ for _, section := range config.Order {
+ // If Order contains a missing section, panic.
+ if config.Sections[section] == nil {
+ panic(section + ": in order, but missing from sections. This is a bug in conf-builder.yml.")
+ }
+
+ if config.Defs[section] != nil {
+ fmt.Print(config.Sections[section].makeDefinedSection(config.Defs[section], config.DefOrder[section], false))
+ } else {
+ fmt.Print(config.Sections[section].makeSection(section, false, false))
+ }
+ }
+}
+
+// Not all sections have defs, and it may be nil. Defs only work on 'list' sections.
+func (h *Header) makeSection(name section, showHeader, showValue bool) string {
+ var buf bytes.Buffer
+
+ // Print section header text.
+ if h.Text != "" {
+ buf.WriteString(h.Text)
+ }
+
+ comment := "#"
+ if showHeader {
+ // this only happens when a defined section has a comment override on the repeating headers.
+ comment = ""
+ }
+
+ if !h.NoHeader { // Print the [section] or [[section]] header.
+ if h.Kind == list { // list sections are commented by default.
+ buf.WriteString(comment + "[[" + string(name) + "]]" + "\n") // list sections use double-brackets.
+ } else {
+ buf.WriteString("[" + string(name) + "]" + "\n") // non-list sections use single brackets.
+ }
+ }
+
+ for _, param := range h.Params {
+ // Print an empty newline for each param if the section has no header and the param has a description.
+ if h.NoHeader && param.Desc != "" {
+ buf.WriteString("\n")
+ }
+
+ // Add ## to the beginning of each line in the description.
+ // Uses the newline \n character to figure out where each line begins.
+ if param.Desc != "" {
+ buf.WriteString("## " + strings.ReplaceAll(strings.TrimSpace(param.Desc), "\n", "\n## ") + "\n")
+ }
+
+ switch {
+ default:
+ fallthrough
+ case showValue:
+ buf.WriteString(fmt.Sprintf("%s = %s\n", param.Name, param.Value()))
+ case param.Example != nil:
+ // If example is not empty, use that commented out, otherwise use the default.
+ fallthrough
+ case h.Kind == list:
+ // If the 'kind' is a 'list', we comment all the parameters.
+ buf.WriteString(fmt.Sprintf("#%s = %s\n", param.Name, param.Value()))
+ }
+ }
+
+ // Each section needs a newline at the end.
+ buf.WriteString("\n")
+
+ return buf.String()
+}
+
+func (p *Param) Value() string {
+ // If example is not empty, use that commented out, otherwise use the default.
+ out, _ := toml.Marshal(p.Default)
+ if p.Example != nil {
+ out, _ = toml.Marshal(p.Example)
+ }
+
+ // The toml marshaller uses only regular quotes " which kinda suck, so replace them with single quotes ' on file paths.
+ if strings.Contains(p.Name, "path") || strings.HasSuffix(p.Name, "file") || p.Name == "command" {
+ return string(bytes.ReplaceAll(out, []byte{'"'}, []byte("'")))
+ }
+
+ return string(out)
+}
+
+// makeDefinedSection duplicates sections from overrides, and prints it once for each override.
+func (h *Header) makeDefinedSection(defs Defs, order []section, showValue bool) string {
+ var buf bytes.Buffer
+
+ for _, section := range order {
+ newHeader := createDefinedSection(defs[section], h)
+ // Make a brand new section and pass it back in.
+ // Only defined sections can comment the header.
+ buf.WriteString(newHeader.makeSection(section, !defs[section].Comment, showValue))
+ }
+
+ return buf.String()
+}
diff --git a/init/config/conf-builder.yml b/init/config/conf-builder.yml
new file mode 100644
index 00000000..d5a34b2c
--- /dev/null
+++ b/init/config/conf-builder.yml
@@ -0,0 +1,804 @@
+# Unpackerr Config File Definition
+---
+
+envvar_prefix: UN_
+order:
+ - global
+ - webserver
+ - folders
+ - starr_header
+ - starr
+ - folder
+ - webhook
+ - cmdhook
+def_order:
+ starr:
+ - sonarr
+ - radarr
+ - lidarr
+ - readarr
+ - whisparr
+recommendations:
+ apps: &APPS
+ - name: Sonarr
+ value: sonarr
+ - name: Radarr
+ value: radarr
+ - name: Lidarr
+ value: lidarr
+ - name: Readarr
+ value: readarr
+ - name: Whisparr
+ value: whisparr
+ - name: Folder
+ value: folder
+ on_off: &BOOLEAN
+ - name: Yes
+ value: true
+ - name: No
+ value: false
+ event_ids: &EVENT_IDS
+ - name: All Events
+ value: 0
+ - name: Queues
+ value: 1
+ - name: Extracting
+ value: 2
+ - name: Extract Failed
+ value: 3
+ - name: Extracted
+ value: 4
+ - name: Imported
+ value: 5
+ - name: Deleting
+ value: 6
+ - name: Delete Failed
+ value: 7
+ - name: Deleted
+ value: 8
+ - name: Nothing Extracted
+ value: 9
+ global: &GLOBAL_INTERVALS
+ - name: 1 minute
+ value: 1m
+ - name: 2 minutes
+ value: 2m
+ - name: 3 minutes
+ value: 3m
+ - name: 4 minutes
+ value: 4m
+ - name: 5 minutes
+ value: 5m
+ - name: 10 minutes
+ value: 10m
+ - name: 15m minutes
+ value: 15m
+ - name: 20 minutes
+ value: 20m
+ timeout: &TIMEOUTS
+ - name: 10 seconds
+ value: 10s
+ - name: 15 seconds
+ value: 15s
+ - name: 20 seconds
+ value: 20s
+ - name: 30 seconds
+ value: 30s
+ - name: 45 seconds
+ value: 45s
+ - name: 1 minute
+ value: 1m
+ - name: 1.5 minutes
+ value: 90s
+ - name: 2 minutes
+ value: 2m
+ - name: 3 minutes
+ value: 3m
+ - name: 5 minutes
+ value: 5m
+ queues: &QUEUE_INTERVALS
+ - name: 1 minute
+ value: 1m
+ - name: 2 minutes
+ value: 2m
+ - name: 3 minutes
+ value: 3m
+ - name: 4 minutes
+ value: 4m
+ - name: 5 minutes
+ value: 5m
+ - name: 10 minutes
+ value: 10m
+ - name: 15m minutes
+ value: 15m
+ - name: 20 minutes
+ value: 20m
+ - name: 2 hours
+ value: 2h
+ - name: 6 hours
+ value: 6h
+ - name: 12 hours
+ value: 12h
+ - name: 24 hours
+ value: 24h
+ folders: &FOLDER_INTERVALS
+ - name: Disabled
+ value: 1ms
+ - name: 1/2 second
+ value: 500ms
+ - name: 1 second
+ value: 1s
+ - name: 2 seconds
+ value: 2s
+ - name: 3 seconds
+ value: 3s
+ - name: 5 seconds
+ value: 5s
+ - name: 8 seconds
+ value: 8s
+ - name: 10 seconds
+ value: 10s
+ - name: 15 seconds
+ value: 15s
+ - name: 30 seconds
+ value: 30s
+ - name: 1 minute
+ value: 1m
+ - name: 2 minutes
+ value: 2m
+
+defs:
+ starr:
+ sonarr:
+ title: Sonarr Settings
+ prefix: SONARR_
+ text: |
+ ## Leaving the [[sonarr]] header uncommented (no leading hash #) without also
+ ## uncommenting the api_key (remove the hash #) will produce a startup warning.
+ defaults:
+ url: http://127.0.0.1:8989
+ radarr:
+ title: Radarr Settings
+ prefix: RADARR_
+ text: |
+ ## Leaving the [[radarr]] header uncommented (no leading hash #) without also
+ ## uncommenting the api_key (remove the hash #) will produce a startup warning.
+ defaults:
+ url: http://127.0.0.1:7878
+ lidarr:
+ title: Lidarr Settings
+ prefix: LIDARR_
+ comment: true
+ defaults:
+ url: http://127.0.0.1:8686
+ readarr:
+ title: Readarr Settings
+ prefix: READARR_
+ comment: true
+ defaults:
+ url: http://127.0.0.1:8787
+ whisparr:
+ title: Whisparr Settings
+ prefix: WHISPARR_
+ comment: true
+ defaults:
+ url: http://127.0.0.1:6969
+
+sections:
+
+ global:
+ title: "Global Settings"
+ no_header: true
+ text: |
+ #######################################################
+ ## Unpackerr Example Configuration File ##
+ #######################################################
+ ## The displayed values are application defaults. ##
+ ## Environment Variables may override all values. ##
+ ## More configuration help: https://unpackerr.zip ##
+ ## Config Generator: https://notifiarr.com/unpackerr ##
+ #######################################################
+ docs: |
+ These values must exist at the top of the config file.
+ If you put them anywhere else they may be attached to a `[header]` inadvertently.
+ When using environment variables, you can simply omit the ones you don't set or change from default.
+ params:
+ - name: debug
+ envvar: DEBUG
+ default: false
+ recommend: *BOOLEAN
+ short: Turns on more logs.
+ desc: |
+ Turn on debug messages in the output. Do not wrap this in quotes.
+ Recommend trying this so you know what it looks like. I personally leave it on.
+ - name: quiet
+ envvar: QUIET
+ default: false
+ recommend: *BOOLEAN
+ short: Do not print logs to stdout or stderr.
+ desc: |
+ Disable writing messages to stdout/stderr. This silences the app. Set a log
+ file below if you set this to true. Recommended when starting with systemctl.
+ - name: error_stderr
+ envvar: ERROR_STDERR
+ default: false
+ recommend: *BOOLEAN
+ short: Print ERROR lines to stderr instead of stdout.
+ desc: |
+ Send error output to stderr instead of stdout by setting error_stderr to true.
+ Recommend leaving this at false. Ignored if quiet (above) is true.
+ - name: activity
+ envvar: ACTIVITY
+ default: false
+ recommend: *BOOLEAN
+ short: Setting true will print only queue counts with activity.
+ desc: |
+ Setting activity to true will silence all app queue log lines with only zeros.
+ Set this to true when you want less log spam.
+ - name: log_queues
+ envvar: LOG_QUEUES
+ default: "1m"
+ short: How often to print internal counters. Uses Go Duration.
+ recommend: *QUEUE_INTERVALS
+ desc: |
+ The Starr-application activity queue is logged on an interval.
+ Adjust that interval with this setting.
+ Default is a minute. 2m, 5m, 10m, 30m, 1h are also perfectly acceptable.
+ - name: log_file
+ envvar: LOG_FILE
+ default: ''
+ example: /downloads/unpackerr.log
+ short: Provide optional file path to write logs
+ desc: |
+ Write messages to a log file. This is the same data that is normally output to stdout.
+ This setting is great for Docker users that want to export their logs to a file.
+ The alternative is to use syslog to log the output of the application to a file.
+ Default is no log file; this is unset.
+ Except on macOS and Windows, the log file gets set to "~/.unpackerr/unpackerr.log"
+ log_files=0 turns off auto-rotation.
+ Default files is 10 and size(mb) is 10 Megabytes; both doubled if debug is true.
+ - name: log_files
+ envvar: LOG_FILES
+ default: 10
+ recommend: &NUMBERS
+ - value: 1
+ - value: 2
+ - value: 3
+ - value: 5
+ - value: 10
+ - value: 15
+ - value: 20
+ short: Log files to keep after rotating. `0` disables rotation
+ - name: log_file_mb
+ envvar: LOG_FILE_MB
+ default: 10
+ recommend: *NUMBERS
+ short: Max size of log files in megabytes
+ - name: interval
+ envvar: INTERVAL
+ default: 2m
+ recommend: *GLOBAL_INTERVALS
+ short: How often apps are polled, recommend `1m` to `5m`.
+ desc: |
+ How often to poll starr apps (sonarr, radarr, etc).
+ Recommend 1m-5m. Uses Go Duration.
+ - name: start_delay
+ envvar: START_DELAY
+ default: 1m
+ recommend: *GLOBAL_INTERVALS
+ short: Files are queued at least this long before extraction.
+ desc: |
+ How long an item must be queued (download complete) before extraction will start.
+ One minute is the historic default and works well. Set higher if your downloads
+ take longer to finalize (or transfer locally). Uses Go Duration.
+ - name: retry_delay
+ envvar: RETRY_DELAY
+ default: 5m
+ recommend: *GLOBAL_INTERVALS
+ short: Failed extractions are retried after at least this long.
+ desc: |
+ How long to wait before removing the history for a failed extraction.
+ Once the history is deleted the item will be recognized as new and
+ extraction will start again. Uses Go Duration.
+ - name: max_retries
+ envvar: MAX_RETRIES
+ default: 3
+ recommend: *NUMBERS
+ short: Failed extractions are retried after at least this long.
+ desc: |
+ How many times to retry a failed extraction. Pauses retry_delay between attempts.
+ - name: parallel
+ envvar: PARALLEL
+ default: 1
+ recommend: *NUMBERS
+ short: Times to retry failed extractions. `0` = unlimited.
+ desc: |
+ How many files may be extracted in parallel. 1 works fine.
+ Do not wrap the number in quotes. Raise this only if you have fast disks and CPU.
+ - name: file_mode
+ envvar: FILE_MODE
+ default: '0644'
+ recommend:
+ - value: '0600'
+ - value: '0640'
+ - value: '0660'
+ - value: '0644'
+ - value: '0640'
+ short: Extracted files are written with this mode.
+ desc: |
+ Use these configurations to control the file modes used for newly extracted
+ files and folders. Recommend 0644/0755 or 0666/0777.
+ - name: dir_mode
+ envvar: DIR_MODE
+ default: '0755'
+ recommend:
+ - value: '0700'
+ - value: '0750'
+ - value: '0755'
+ - value: '0770'
+ - value: '0775'
+ short: Extracted folders are written with this mode
+
+ webserver:
+ title: Web Server
+ docs: |
+ :::note Metrics
+ The web server currently only provides prometheus metrics, which you can display in
+ [Grafana](https://grafana.com/grafana/dashboards/18817-unpackerr/).
+ It provides no UI. This may change in the future. The web server was added in v0.12.0.
+ :::
+ envvar_prefix: WEBSERVER_
+ params:
+ - name: metrics
+ envvar: METRICS
+ default: false
+ recommend: *BOOLEAN
+ short: Extracted folders are written with this mode
+ desc: The web server currently only supports metrics; set this to true if you wish to use it.
+ - name: listen_addr
+ envvar: LISTEN_ADDR
+ default: 0.0.0.0:5656
+ short: ip:port to listen on; `0.0.0.0` is all IPs.
+ desc: This may be set to a port or an ip:port to bind a specific IP. 0.0.0.0 binds ALL IPs.
+ - name: log_file
+ envvar: LOG_FILE
+ default: ''
+ short: Provide optional file path to write HTTP logs.
+ desc: Recommend setting a log file for HTTP requests. Otherwise, they go with other logs.
+ - name: log_files
+ envvar: LOG_FILES
+ default: 10
+ recommend: *NUMBERS
+ short: Log files to keep after rotating. `0` to disable.
+ desc: This app automatically rotates logs. Set these to the size and number to keep.
+ - name: log_file_mb
+ envvar: LOG_FILE_MB
+ default: 10
+ recommend: *NUMBERS
+ short: Max size of HTTP log files in megabytes
+ - name: ssl_cert_file
+ envvar: SSL_CERT_FILE
+ default: ''
+ short: Path to SSL cert file to serve HTTPS.
+ desc: Set both of these to valid file paths to enable HTTPS/TLS.
+ - name: ssl_key_file
+ envvar: SSL_KEY_FILE
+ default: ''
+ short: Path to SSL key file to serve HTTPS.
+ - name: urlbase
+ envvar: URLBASE
+ default: /
+ short: Base URL path to serve HTTP content.
+ desc: Base URL from which to serve content.
+ - name: upstreams
+ envvar: UPSTREAMS
+ default: []
+ kind: conlist
+ short: List of upstream proxy CIDRs or IPs to trust.
+ desc: |
+ Upstreams should be set to the IP or CIDR of your trusted upstream proxy.
+ Setting this correctly allows X-Forwarded-For to be used in logs.
+ In the future it may control auth proxy trust. Must be a list of strings.
+ example: upstreams = [ "127.0.0.1/32", "10.1.2.0/24" ]
+
+ starr_header:
+ no_header: true
+ text: |
+ ###############################################################################
+ ##-IMPORTANT-#######-READ THIS!!!-################ Seriously, read this. ######
+ ###############################################################################
+ ## The following sections can be repeated if you have more than one Sonarr, ##
+ ## Radarr, Lidarr, Readarr, Whisparr, Folder, Webhook, and/or Command Hook. ##
+ ## You MUST uncomment the [[header]], url and api_key at for any Starr app. ##
+ ## The [[sonarr]] and [[radarr]] headers come uncommented. Uncomment the url ##
+ ## and api_key if they are in use. Comment them with a hash if they are not. ##
+ ## Uncomment the [[lidarr]] and/or [[readarr]] headers and values if in use. ##
+ ###############################################################################
+ ###############################################################################
+ ## ALL LINES BEGINNING WITH A HASH # ARE IGNORED COMMENTS ##
+ ## REMOVE THE HASH # FROM CONFIG LINES YOU WANT TO CHANGE ##
+ ###############################################################################
+ ###############################################################################
+
+ starr:
+ kind: list
+ params:
+ - name: url
+ envvar: URL
+ short: URL where this starr app can be accessed.
+ - name: api_key
+ envvar: API_KEY
+ default: ''
+ example: 0123456789abcdef0123456789abcdef
+ short: Provide URL and API key if you use this app.
+ - name: paths
+ envvar: PATHS_
+ default: ['/downloads']
+ kind: list
+ short: File system path where downloaded items are located.
+ desc: |
+ List of paths where content is downloaded for this app.
+ Used as fallback if the path the Starr app reports does not exist or is not accessible.
+ - name: protocols
+ envvar: PROTOCOLS
+ default: torrent
+ recommend:
+ - value: torrent
+ - value: torrent,usenet
+ - value: usenet
+ short: 'Protocols to process. Alt: `torrent,usenet`'
+ desc: 'Default protocols is torrent. Alternative: torrent,usenet'
+ - name: timeout
+ envvar: TIMEOUT
+ default: 10s
+ recommend: *TIMEOUTS
+ short: How long to wait for the app to respond.
+ desc: How long to wait for a reply from the backend.
+ - name: delete_delay
+ envvar: DELETE_DELAY
+ default: 5m
+ recommend: *GLOBAL_INTERVALS
+ short: Extracts are deleted this long after import, `-1s` to disable.
+ desc: How long to wait after import before deleting the extracted items.
+ - name: delete_orig
+ envvar: DELETE_ORIG
+ default: false
+ recommend: *BOOLEAN
+ short: Delete archives after import? Recommend keeping this false.
+ desc: |
+ If you use this app with NZB you may wish to delete archives after extraction.
+ General recommendation is: do not enable this for torrent use.
+ Setting this to true deletes the entire original download folder after import.
+ - name: syncthing
+ envvar: SYNCTHING
+ default: false
+ recommend: *BOOLEAN
+ short: Setting this to true makes unpackerr wait for syncthing to finish.
+ desc: If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish.
+
+ # Global folder configuration.
+ folders:
+ title: Folder Settings
+ text: |
+ ## Global Folder configuration that affects all watched folders.
+ envvar_prefix: FOLDERS_
+ params:
+ - name: interval
+ envvar: INTERVAL
+ default: 0s
+ short: How often poller checks for new folders. Use `1ms` to disable it.
+ recommend: *FOLDER_INTERVALS
+ desc: |
+ How often poller checks for new folders.
+ The default of `0s` will disable the poller on all systems except Docker.
+ Set this value to `1ms` to disable it in Docker.
+ - name: buffer
+ envvar: BUFFER
+ default: 20000
+ short: How many new folder events can be immediately queued.
+ desc: How many new folder events can be immediately queued. Don't change this.
+
+ # Per-folder configuration (list).
+ folder:
+ title: Watch Folders
+ text: |
+ ##################################################################################
+ ### ### STOP HERE ### STOP HERE ### STOP HERE ### STOP HERE #### STOP HERE ### #
+ ### Only using Starr apps? The things above. The below configs are OPTIONAL. ### #
+ ##################################################################################
+
+
+ ##-Folders-#######################################################################
+ ## This application can also watch folders for things to extract. If you copy a ##
+ ## subfolder into a watched folder (defined below) any extractable items in the ##
+ ## folder will be decompressed. This has nothing to do with Starr applications. ##
+ ##################################################################################
+ docs: |
+ Folders are a way to watch a folder for things to extract. You can use this to
+ monitor your download client's "move to" path if you're not using it with an Starr app.
+ envvar_prefix: FOLDER_
+ kind: list
+ params:
+ - name: path
+ envvar: PATH
+ default: ''
+ example: /downloads/auto_extract
+ short: Folder to watch for archives. **Not for Starr apps.**
+ - name: extract_path
+ envvar: EXTRACT_PATH
+ default: ''
+ short: Where to extract to. Uses `path` if not set.
+ desc: Path to extract files to. The default (leaving this blank) is the same as `path` (above).
+ - name: delete_after
+ envvar: DELETE_AFTER
+ default: 10m
+ recommend: *GLOBAL_INTERVALS
+ short: Delete requested files after this duration; `0` disables.
+ desc: |
+ Delete extracted or original files this long after extraction.
+ The default is 0. Set to 0 to disable all deletes. Uncomment it to enable deletes. Uses Go Duration.
+ - name: disable_recursion
+ envvar: DISABLE_RECURSION
+ default: false
+ recommend: *BOOLEAN
+ short: Setting this to true disables extracting archives inside archives.
+ desc: Unpackerr extracts archives inside archives. Set this to true to disable recursive extractions.
+ - name: delete_files
+ envvar: DELETE_FILES
+ default: false
+ recommend: *BOOLEAN
+ short: Delete extracted files after successful extraction.
+ desc: Delete extracted files after successful extraction? Honors delete_after.
+ - name: delete_original
+ envvar: DELETE_ORIGINAL
+ default: false
+ recommend: *BOOLEAN
+ short: Delete archives after successful extraction.
+ desc: Delete original items after successful extraction? Honors delete_after.
+ - name: disable_log
+ envvar: DISABLE_LOG
+ default: false
+ recommend: *BOOLEAN
+ short: Turns off creation of extraction logs files for this folder.
+ desc: Disable extraction log (unpackerred.txt) file creation?
+ - name: move_back
+ envvar: MOVE_BACK
+ default: false
+ recommend: *BOOLEAN
+ short: Move extracted items back into original folder.
+ desc: Move extracted files into original folder? If false, files go into an _unpackerred folder.
+ - name: extract_isos
+ envvar: EXTRACT_ISOS
+ default: false
+ recommend: *BOOLEAN
+ short: Setting this to true enables .iso file extraction.
+ desc: Set this to true if you want this app to extract ISO files with .iso extension.
+
+ webhook:
+ title: Web Hooks
+ text: |
+ ################
+ ### Webhooks ###
+ ################
+ # Sends a webhook when an extraction queues, starts, finishes, and/or is deleted.
+ # Created to integrate with notifiarr.com.
+ # Also works natively with Discord.com, Telegram.org, and Slack.com webhooks.
+ # Can possibly be used with other services by providing a custom template_path.
+ ###### Don't forget to uncomment [[webhook]] and url at a minimum !!!!
+ docs: |
+ This application can send a `POST` webhook to a URL when an extraction begins, and again
+ when it finishes. Configure 1 or more webhook URLs with the parameters below.
+ Works great with [notifiarr.com](https://notifiarr.com). You can use
+ [requestbin.com](https://requestbin.com/r/) to test and _see_ the payload.
+ notes: |
+ - _`Nickname` should equal the `chat_id` value in Telegram webhooks._
+ - _`Channel` is used as destination channel for Slack. It's not used in others._
+ - _`Nickname` and `Channel` may be used as custom values in custom templates._
+ - _`Name` is only used in logs, but it's also available as a template value as `{{name}}`._
+ - Built-In Templates: `pushover`, `telegram`, `discord`, `notifiarr`, `slack`, `gotify`.
+ envvar_prefix: WEBHOOK_
+ kind: list
+ params:
+ - name: url
+ envvar: URL
+ default: ''
+ example: https://notifiarr.com/api/v1/notification/unpackerr/api_key_from_notifiarr_com
+ short: URL to send POST webhook to.
+ - name: name
+ envvar: NAME
+ default: ''
+ short: Provide an optional name to hide the URL in logs.
+ desc: |
+ Provide an optional name to hide the URL in logs.
+ If a name is not provided then the URL is used.
+ - name: silent
+ envvar: SILENT
+ default: false
+ recommend: *BOOLEAN
+ short: Hide successful POSTs from logs.
+ desc: Do not log success (less log spam).
+ - name: events
+ envvar: EVENTS_
+ default:
+ - 0
+ example:
+ - 1
+ - 4
+ - 6
+ kind: list
+ recommend: *EVENT_IDS
+ short: List of event ids to send notification for, `0` for all.
+ desc: |
+ List of event ids to send notification for, [0] for all.
+ The default is [0] and this is an example:
+ - name: nickname
+ envvar: NICKNAME
+ default: 'Unpackerr'
+ short: Passed into templates for telegram, discord and slack hooks.
+ desc: |
+ ===> Advanced Optional Webhook Configuration <===
+ Used in Discord and Slack templates as bot name, in Telegram as chat_id.
+ - name: channel
+ envvar: CHANNEL
+ default: ''
+ short: Passed into templates for slack.com webhooks.
+ desc: Also passed into templates. Used in Slack templates for destination channel.
+ - name: exclude
+ envvar: EXCLUDE_
+ default: []
+ example: ["readarr", "lidarr"]
+ recommend: *APPS
+ kind: list
+ short: 'List of apps to exclude: radarr, sonarr, folders, etc.'
+ desc: 'List of apps to exclude. None by default. This is an example:'
+ - name: template_path
+ envvar: TEMPLATE_PATH
+ default: ''
+ short: Instead of an internal template, provide your own.
+ desc: Override internal webhook template for discord.com or other hooks.
+ - name: template
+ envvar: TEMPLATE
+ default: ''
+ recommend:
+ - value: "notifiarr"
+ - value: "discord"
+ - value: "telegram"
+ - value: "gotify"
+ - value: "pushover"
+ - value: "slack"
+ short: Instead of auto template selection, force a built-in template.
+ desc: 'Override automatic template detection. Values: notifiarr, discord, telegram, gotify, pushover, slack'
+ - name: ignore_ssl
+ envvar: IGNORE_SSL
+ default: false
+ recommend: *BOOLEAN
+ short: Ignore invalid SSL certificates.
+ desc: Set this to true to ignore the SSL certificate on the server.
+ - name: timeout
+ envvar: TIMEOUT
+ default: 10s
+ recommend: *TIMEOUTS
+ short: How long to wait for server response.
+ desc: You can adjust how long to wait for a server response.
+ - name: content_type
+ envvar: CONTENT_TYPE
+ default: application/json
+ recommend:
+ - value: "application/json"
+ - value: "application/x-yaml"
+ - value: "application/xml"
+ - value: "application/x-www-form-urlencoded"
+ short: Content-Type header sent to webhook.
+ desc: If your custom template uses another MIME type, set this.
+
+ cmdhook:
+ title: Command Hooks
+ docs: |
+ Unpackerr can execute commands (or scripts) before and after an archive extraction.
+ The only thing required is a command. Name is optional, and used in logs only.
+ Setting `shell` to `true` executes your command after `/bin/sh -c` or `cmd.exe /c`
+ on Windows.
+ tail: |
+ All extraction data is input to the command using environment variables, see example below.
+ Extracted files variables names begin with `UN_DATA_FILES_`.
+ Try `/usr/bin/env` as an example command to see what variables are available.
+
+
+ Example Output Variables
+
+ ```none
+ UN_DATA_OUTPUT=folder/subfolder_unpackerred
+ UN_PATH=folder/subfolder
+ UN_DATA_START=2021-10-04T23:04:27.849216-07:00
+ UN_REVISION=
+ UN_EVENT=extracted
+ UN_GO=go1.17
+ UN_DATA_ARCHIVES=folder/subfolder_unpackerred/Funjetting.rar,folder/subfolder_unpackerred/Funjetting.r00,folder/subfolder/files.zip
+ UN_DATA_ARCHIVE_2=folder/subfolder/files.zip
+ UN_DATA_ARCHIVE_1=folder/subfolder_unpackerred/Funjetting.r00
+ UN_DATA_ARCHIVE_0=folder/subfolder_unpackerred/Funjetting.rar
+ UN_DATA_FILES=folder/subfolder/Funjetting.mp3,folder/subfolder/Funjetting.r00,folder/subfolder/Funjetting.rar,folder/subfolder/_unpackerred.subfolder.txt
+ UN_DATA_FILE_1=folder/subfolder/Funjetting.r00
+ UN_DATA_BYTES=2407624
+ PWD=/Users/david/go/src/github.com/Unpackerr/unpackerr
+ UN_DATA_FILE_0=folder/subfolder/Funjetting.mp3
+ UN_OS=darwin
+ UN_DATA_FILE_3=folder/subfolder/_unpackerred.subfolder.txt
+ UN_DATA_FILE_2=folder/subfolder/Funjetting.rar
+ UN_BRANCH=
+ UN_TIME=2021-10-04T23:04:27.869613-07:00
+ UN_VERSION=
+ UN_DATA_QUEUE=0
+ SHLVL=1
+ UN_APP=Folder
+ UN_STARTED=2021-10-04T23:03:22.849253-07:00
+ UN_ARCH=amd64
+ UN_DATA_ELAPSED=20.365752ms
+ UN_DATA_ERROR=
+ ```
+
+
+ text: |
+ #####################
+ ### Command Hooks ###
+ #####################
+ # Executes a script or command when an extraction queues, starts, finishes, and/or is deleted.
+ # All data is passed in as environment variables. Try /usr/bin/env to see what variables are available.
+ ###### Don't forget to uncomment [[cmdhook]] at a minimum !!!!
+ envvar_prefix: CMDHOOK_
+ kind: list
+ params:
+ - name: command
+ envvar: COMMAND
+ default: ''
+ example: /downloads/scripts/command.sh
+ short: Command to run.
+ - name: name
+ envvar: NAME
+ default: ''
+ short: Name for logs, otherwise uses first word in command.
+ desc: |
+ Provide an optional name to hide the URL in logs.
+ If a name is not provided the first word in the command is used.
+ - name: shell
+ envvar: SHELL
+ default: false
+ recommend: *BOOLEAN
+ short: Run command inside a shell.
+ desc: Runs the command inside /bin/sh ('nix) or cmd.exe (Windows).
+ - name: silent
+ envvar: SILENT
+ default: false
+ recommend: *BOOLEAN
+ short: Hide command output from logs.
+ desc: Do not log command's output.
+ - name: events
+ envvar: EVENTS_
+ default:
+ - 0
+ recommend: *EVENT_IDS
+ example:
+ - 1
+ - 4
+ - 7
+ kind: list
+ short: List of event ids to run command for, `0` for all.
+ desc: |
+ List of event ids to run command for, [0] for all.
+ The default is [0] and this is an example:
+ - name: exclude
+ envvar: EXCLUDE_
+ default: []
+ example: ["readarr", "lidarr"]
+ recommend: *APPS
+ kind: list
+ short: 'List of apps to exclude: radarr, sonarr, folders, etc.'
+ desc: |
+ ===> Optional Command Hook Configuration <===
+ List of apps to exclude. None by default. This is an example:
+ - name: timeout
+ envvar: TIMEOUT
+ default: 10s
+ recommend: *TIMEOUTS
+ short: How long to wait for the command to run.
+ desc: You can adjust how long to wait for the command to run.
diff --git a/init/config/docs-builder.go b/init/config/docs-builder.go
new file mode 100644
index 00000000..2314681e
--- /dev/null
+++ b/init/config/docs-builder.go
@@ -0,0 +1,147 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/BurntSushi/toml"
+)
+
+const (
+ outputDir = "generated/"
+ dirMode = 0o755
+ fileMode = 0o644
+)
+
+func printDocusaurus(config *Config) {
+ // Loop the 'Order' list.
+ if err := makeGenerated(config); err != nil {
+ panic(err)
+ }
+
+ for _, section := range config.Order {
+ // If Order contains a missing section, panic.
+ if config.Sections[section] == nil {
+ panic(section + ": in order, but missing from sections. This is a bug in conf-builder.yml.")
+ }
+
+ if len(config.Sections[section].Params) < 1 {
+ continue
+ }
+
+ if config.Defs[section] != nil {
+ data := config.Sections[section].makeDefinedDocs(config.Prefix, config.Defs[section], config.DefOrder[section])
+ if err := output(string(section), data); err != nil {
+ panic(err)
+ }
+ } else {
+ data := config.Sections[section].makeDocs(config.Prefix, section)
+ if err := output(string(section), data); err != nil {
+ panic(err)
+ }
+ }
+ }
+}
+
+func output(file, content string) error {
+ _ = os.Mkdir(outputDir, dirMode)
+ date := "---\n# Generated: " + time.Now().Round(time.Second).String() + "\n---\n\n"
+ //nolint:wrapcheck
+ return os.WriteFile(filepath.Join(outputDir, file+".md"), []byte(date+content), fileMode)
+}
+
+// makeGenerated writes a special file that the website can import.
+// Adds all param sections except global into a docusaurus import format.
+func makeGenerated(config *Config) error {
+ var (
+ first bytes.Buffer
+ second bytes.Buffer
+ )
+
+ for _, section := range config.Order {
+ if len(config.Sections[section].Params) > 0 && section != "global" {
+ title := "G" + string(section)
+ first.WriteString("import " + title + " from './" + string(section) + ".md';\n")
+ second.WriteString("<" + title + "/>\n")
+ }
+ }
+
+ return output("index", first.String()+"\n"+second.String())
+}
+
+func (h *Header) makeDocs(prefix string, section section) string {
+ buf := bytes.Buffer{}
+ buf.WriteString("## " + h.Title + "\n\n\n")
+
+ conf := h.makeSection(section, true, true)
+ env := h.makeCompose(h.Title, prefix, true)
+ header := "[" + string(section) + "]"
+
+ if h.Kind == list {
+ header = "[[" + string(section) + "]]"
+ }
+
+ if h.NoHeader {
+ buf.WriteString(" Examples. Prefix: " + prefix + "
\n\n")
+ } else {
+ buf.WriteString(" Examples. Prefix: " + prefix + h.Prefix + ", Header: " + header + "
\n\n")
+ }
+
+ buf.WriteString("- Using the config file:\n\n```yaml\n")
+ buf.WriteString(strings.TrimSpace(conf) + "\n```\n\n")
+ buf.WriteString("- Using environment variables:\n\n```js\n")
+ buf.WriteString(env + "```\n\n \n\n")
+ buf.WriteString(h.Docs + "\n") // Docs comes before the table.
+ buf.WriteString(h.makeDocsTable(prefix) + "\n")
+ buf.WriteString(h.Tail) // Tail goes after the docs and table.
+
+ if h.Notes != "" { // Notes become a sub header.
+ buf.WriteString("### " + h.Title + " Notes\n\n" + h.Notes)
+ }
+
+ return buf.String()
+}
+
+func (h *Header) makeDocsTable(prefix string) string {
+ const (
+ tableHeader = "|Config Name|Variable Name|Default / Note|\n|---|---|---|\n"
+ tableFormat = "|%s|`%s`|%v / %s|\n"
+ )
+
+ buf := bytes.Buffer{}
+ buf.WriteString(tableHeader)
+
+ for _, param := range h.Params {
+ envVar := prefix + h.Prefix + param.EnvVar
+ if param.Kind == list {
+ envVar += "0"
+ }
+
+ def := "No Default"
+
+ if rv := reflect.ValueOf(param.Default); rv.Kind() == reflect.Bool || !rv.IsZero() {
+ if t, _ := toml.Marshal(param.Default); len(t) > 0 {
+ def = "`" + string(t) + "`"
+ }
+ }
+
+ buf.WriteString(fmt.Sprintf(tableFormat, param.Name, envVar, def, param.Short))
+ }
+
+ return buf.String()
+}
+
+func (h *Header) makeDefinedDocs(prefix string, defs Defs, order []section) string {
+ var buf bytes.Buffer
+
+ for _, section := range order {
+ buf.WriteString(createDefinedSection(defs[section], h).makeDocs(prefix, section))
+ }
+
+ return buf.String()
+}
diff --git a/init/config/main.go b/init/config/main.go
new file mode 100644
index 00000000..ac74f2b5
--- /dev/null
+++ b/init/config/main.go
@@ -0,0 +1,142 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "gopkg.in/yaml.v3"
+)
+
+const (
+ list = "list"
+ // inputFile = "https://raw.githubusercontent.com/Unpackerr/unpackerr/main/init/config/conf-builder.yml"
+ inputFile = "https://raw.githubusercontent.com/Unpackerr/unpackerr/dn2_conf_builder/init/config/conf-builder.yml"
+ opTimeout = 6 * time.Second
+)
+
+type section string
+
+type Option struct {
+ Name string `yaml:"name"`
+ Value any `yaml:"value"`
+}
+
+type Config struct {
+ DefOrder map[section][]section `yaml:"def_order"`
+ Defs map[section]Defs `yaml:"defs"`
+ Prefix string `yaml:"envvar_prefix"`
+ Order []section `yaml:"order"`
+ Sections map[section]*Header `yaml:"sections"`
+}
+
+type Header struct {
+ Tail string `yaml:"tail"`
+ Title string `yaml:"title"`
+ Text string `yaml:"text"`
+ Docs string `yaml:"docs"`
+ Notes string `yaml:"notes"`
+ Prefix string `yaml:"envvar_prefix"`
+ Params []*Param `yaml:"params"`
+ Kind string `yaml:"kind"` // "", list
+ NoHeader bool `yaml:"no_header"` // Do not print [section] header.
+}
+
+type Param struct {
+ Name string `yaml:"name"`
+ EnvVar string `yaml:"envvar"`
+ Default any `yaml:"default"`
+ Example any `yaml:"example"`
+ Short string `yaml:"short"`
+ Desc string `yaml:"desc"`
+ Kind string `yaml:"kind"` // "", list, conlist
+ Recommend []Option `yaml:"recommend"`
+}
+
+type Def struct {
+ Comment bool `yaml:"comment"` // just the header.
+ Title string `yaml:"title"`
+ Prefix string `yaml:"prefix"`
+ Text string `yaml:"text"`
+ Defaults map[string]any `yaml:"defaults"`
+}
+
+type Defs map[section]*Def
+
+func main() {
+ file, err := openFile()
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+
+ config := &Config{}
+ // Decode conf-builder file into Go data structure.
+ if err = yaml.NewDecoder(file).Decode(config); err != nil {
+ panic(err)
+ }
+
+ switch {
+ default:
+ fallthrough
+ case len(os.Args) <= 1:
+ fallthrough
+ case os.Args[1] == "conf":
+ printConfFile(config)
+ case os.Args[1] == "compose", os.Args[1] == "docker":
+ printCompose(config)
+ case os.Args[1] == "docs":
+ printDocusaurus(config)
+ }
+}
+
+// openFile opens a file or url for the parser.
+func openFile() (io.ReadCloser, error) {
+ fileName := inputFile
+ if len(os.Args) > 2 { //nolint:mnd
+ fileName = os.Args[len(os.Args)-1] // take last arg as file.
+ }
+
+ if strings.HasPrefix(fileName, "http") {
+ http.DefaultClient.Timeout = opTimeout
+
+ resp, err := http.Get(fileName) //nolint:noctx // because we set a timeout.
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", fileName, err)
+ }
+
+ return resp.Body, nil
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", fileName, err)
+ }
+
+ return file, nil
+}
+
+func createDefinedSection(def *Def, section *Header) *Header {
+ newSection := &Header{
+ Text: def.Text,
+ Prefix: def.Prefix,
+ Title: def.Title,
+ Params: section.Params,
+ Kind: section.Kind,
+ }
+
+ // Loop each defined section Defaults, and see if one of the param names match.
+ for overrideName, override := range def.Defaults {
+ for _, defined := range newSection.Params {
+ // If the name of the default (override) matches this param name, overwrite the value.
+ if defined.Name == overrideName {
+ defined.Default = override
+ }
+ }
+ }
+
+ return newSection
+}