From 6721faf773dc9e1295244a3ab6b734959dbe6c66 Mon Sep 17 00:00:00 2001 From: PieterDherde Date: Mon, 8 Apr 2024 14:40:56 +0200 Subject: [PATCH] [APP] Add SLSKD (#3010) * Add SLSKD * Add Traefik labels * Make sure user edits are recorded in config * Add default shared directory * Add information on credentials * Specify CPU architectures in config.json * Remove superfluous file * fix: run the app as root * fix: remove unnecessary config options --------- Co-authored-by: Pieter D'herde Co-authored-by: Stavros --- apps/slskd/config.json | 54 +++++++ apps/slskd/data/slskd.yml.template | 226 +++++++++++++++++++++++++++++ apps/slskd/docker-compose.yml | 48 ++++++ apps/slskd/metadata/description.md | 36 +++++ apps/slskd/metadata/logo.jpg | Bin 0 -> 9496 bytes 5 files changed, 364 insertions(+) create mode 100644 apps/slskd/config.json create mode 100644 apps/slskd/data/slskd.yml.template create mode 100644 apps/slskd/docker-compose.yml create mode 100644 apps/slskd/metadata/description.md create mode 100644 apps/slskd/metadata/logo.jpg diff --git a/apps/slskd/config.json b/apps/slskd/config.json new file mode 100644 index 00000000..6bc842b7 --- /dev/null +++ b/apps/slskd/config.json @@ -0,0 +1,54 @@ +{ + "name": "SLSKD", + "available": true, + "port": 5030, + "exposable": true, + "id": "slskd", + "description": "A modern client-server application for the Soulseek file-sharing network.", + "tipi_version": 1, + "version": "0.20.1", + "categories": ["utilities"], + "short_desc": "P2P downloads", + "author": "jpdillingham", + "source": "https://github.com/slskd/slskd", + "supported_architectures": ["amd64", "arm64"], + "form_fields": [{ + "type": "text", + "label": "WebUI username", + "max": 50, + "min": 3, + "required": true, + "env_variable": "SLSKD_WEB_USER" + }, + { + "type": "password", + "label": "WebUI password", + "max": 50, + "min": 3, + "required": true, + "env_variable": "SLSKD_WEB_PASSWORD" + }, + { + "type": "text", + "label": "Soulseek username", + "max": 50, + "min": 3, + "required": true, + "env_variable": "SLSKD_USER" + }, + { + "type": "password", + "label": "Soulseek password", + "max": 50, + "min": 3, + "required": true, + "env_variable": "SLSKD_PASSWORD" + }, + { + "type": "boolean", + "label": "Allow remote configuration", + "required": true, + "env_variable": "SLSKD_REMOTE_CONFIGURATION" + } + ] +} diff --git a/apps/slskd/data/slskd.yml.template b/apps/slskd/data/slskd.yml.template new file mode 100644 index 00000000..25fe48be --- /dev/null +++ b/apps/slskd/data/slskd.yml.template @@ -0,0 +1,226 @@ +#debug: false +remote_configuration: {{SLSKD_REMOTE_CONFIGURATION}} +# remote_file_management: false +# instance_name: default +# flags: +# no_logo: false +# no_start: false +# no_config_watch: false +# no_connect: false +# no_share_scan: false +# force_share_scan: false +# no_version_check: true +# log_sql: false +# experimental: false +# volatile: false +# case_sensitive_reg_ex: false +# relay: +# enabled: false +# mode: controller # controller (default), agent, or debug (for local development) +# # controller config is required when running in 'agent' mode +# # this specifies the relay controller that will be controlling this agent +# controller: +# address: https://some.site.com:5000 +# ignore_certificate_errors: false +# api_key: +# secret: +# downloads: false +# # agent config is optional when running in 'controller' mode +# # this specifies all of the agents capable of connecting +# agents: +# my_agent: +# instance_name: my_agent # make sure the top-level instance_name of the agent matches! +# secret: +# cidr: 0.0.0.0/0,::/0 +directories: + incomplete: /incomplete + downloads: /downloads +shares: + directories: + filters: + - \.ini$ + - Thumbs.db$ + - \.DS_Store$ + cache: + storage_mode: memory + workers: 16 +# retention: ~ # retain indefinitely (do not automatically re-scan) +# rooms: +# - ~ +# global: +# upload: +# slots: 20 +# speed_limit: 1000 # in kibibytes +# limits: +# queued: +# files: 500 +# megabytes: 5000 +# daily: +# files: 1000 +# megabytes: 10000 +# failures: 200 +# weekly: +# files: 5000 +# megabytes: 50000 +# failures: 1000 +# download: +# slots: 500 +# speed_limit: 1000 +# groups: +# default: +# upload: +# priority: 500 +# strategy: roundrobin +# slots: 10 +# limits: +# queued: +# files: 150 +# megabytes: 1500 +# daily: ~ # no daily limits (weekly still apply) +# weekly: +# files: 1500 +# megabytes: 15000 +# failures: 150 +# leechers: +# thresholds: +# files: 1 +# directories: 1 +# upload: +# priority: 999 +# strategy: roundrobin +# slots: 1 +# speed_limit: 100 +# limits: +# queued: +# files: 15 +# megabytes: 150 +# daily: +# files: 30 +# megabytes: 300 +# failures: 10 +# weekly: +# files: 150 +# megabytes: 1500 +# failures: 30 +# blacklisted: +# members: +# - +# cidrs: +# - +# user_defined: +# my_buddies: +# upload: +# priority: 250 +# strategy: firstinfirstout +# slots: 10 +# limits: +# queued: +# files: 1000 # override global default +# members: +# - alice +# - bob +# filters: +# search: +# request: +# - ^.{1,2}$ +web: + port: 5030 + https: + disabled: false +# port: 5031 +# force: false +# certificate: +# pfx: ~ +# password: ~ + url_base: / + content_path: wwwroot +# logging: false + authentication: + disabled: false + username: {{SLSKD_WEB_USER}} + password: {{SLSKD_WEB_PASSWORD}} +# jwt: +# key: ~ +# ttl: 604800000 +# api_keys: +# my_api_key: +# key: +# role: readonly # readonly, readwrite, administrator +# cidr: 0.0.0.0/0,::/0 +# retention: +# transfers: +# upload: +# succeeded: 1440 # 1 day +# errored: 30 +# cancelled: 5 +# download: +# succeeded: 1440 # 1 day +# errored: 20160 # 2 weeks +# cancelled: 5 +# files: +# complete: 20160 # 2 weeks +# incomplete: 43200 # 30 days +# logs: 259200 # 180 days +# logger: +# disk: true +# loki: ~ +# metrics: +# enabled: false +# url: /metrics +# authentication: +# disabled: false +# username: slskd +# password: slskd +# feature: +# swagger: false +soulseek: + address: vps.slsknet.org + port: 2271 + username: {{SLSKD_USER}} + password: {{SLSKD_PASSWORD}} +# description: | +# A slskd user. https://github.com/slskd/slskd + listen_ip_address: 0.0.0.0 + listen_port: 50300 +# diagnostic_level: Info +# distributed_network: +# disabled: false +# disable_children: false +# child_limit: 25 +# logging: false + connection: + timeout: + connect: 10000 + inactivity: 15000 + buffer: + read: 16384 + write: 16384 + transfer: 262144 + write_queue: 250 +# proxy: +# enabled: false +# address: ~ +# port: ~ +# username: ~ +# password: ~ +# integration: +# ftp: +# enabled: false +# address: ~ +# port: ~ +# username: ~ +# password: ~ +# remote_path: / +# encryption_mode: auto +# ignore_certificate_errors: false +# overwrite_existing: true +# connection_timeout: 5000 +# retry_attempts: 3 +# pushbullet: +# enabled: false +# access_token: ~ +# notification_prefix: "From slskd:" +# notify_on_private_message: true +# notify_on_room_mention: true +# retry_attempts: 3 +# cooldown_time: 900000 diff --git a/apps/slskd/docker-compose.yml b/apps/slskd/docker-compose.yml new file mode 100644 index 00000000..5de11dbd --- /dev/null +++ b/apps/slskd/docker-compose.yml @@ -0,0 +1,48 @@ +version: "3.9" +services: + slskd: + image: slskd/slskd:0.20.1 + container_name: slskd + volumes: + - "${APP_DATA_DIR}:/app" + - "${ROOT_FOLDER_HOST}/media/downloads/complete:/downloads" + - "${ROOT_FOLDER_HOST}/media/downloads/incomplete:/incomplete" + - "${ROOT_FOLDER_HOST}/media/:/shared" + environment: + - SLSKD_WEB_USER=${SLSKD_WEB_USER} + - SLSKD_WEB_PASSWORD=${SLSKD_WEB_PASSWORD} + - SLSKD_USER=${SLSKD_USER} + - SLSKD_PASSWORD=${SLSKD_PASSWORD} + - SLSKD_REMOTE_CONFIGURATION=${SLSKD_REMOTE_CONFIGURATION} + - SLSKD_CONFIG=/app/data/slskd.yml + - SLSKD_SHARED_DIR=/shared + restart: unless-stopped + networks: + - tipi_main_network + ports: + - ${APP_PORT}:5030 + labels: + # Main + traefik.enable: true + traefik.http.middlewares.slskd-web-redirect.redirectscheme.scheme: https + traefik.http.services.slskd.loadbalancer.server.port: 5030 + # Web + traefik.http.routers.slskd-insecure.rule: Host(`${APP_DOMAIN}`) + traefik.http.routers.slskd-insecure.entrypoints: web + traefik.http.routers.slskd-insecure.service: slskd + traefik.http.routers.slskd-insecure.middlewares: slskd-web-redirect + # Websecure + traefik.http.routers.slskd.rule: Host(`${APP_DOMAIN}`) + traefik.http.routers.slskd.entrypoints: websecure + traefik.http.routers.slskd.service: slskd + traefik.http.routers.slskd.tls.certresolver: myresolver + # Local domain + traefik.http.routers.slskd-local-insecure.rule: Host(`slskd.${LOCAL_DOMAIN}`) + traefik.http.routers.slskd-local-insecure.entrypoints: web + traefik.http.routers.slskd-local-insecure.service: slskd + traefik.http.routers.slskd-local-insecure.middlewares: slskd-web-redirect + # Local domain secure + traefik.http.routers.slskd-local.rule: Host(`slskd.${LOCAL_DOMAIN}`) + traefik.http.routers.slskd-local.entrypoints: websecure + traefik.http.routers.slskd-local.service: slskd + traefik.http.routers.slskd-local.tls: true diff --git a/apps/slskd/metadata/description.md b/apps/slskd/metadata/description.md new file mode 100644 index 00000000..3d8773a6 --- /dev/null +++ b/apps/slskd/metadata/description.md @@ -0,0 +1,36 @@ +A modern client-server application for the [Soulseek](https://www.slsknet.org/news/) file-sharing network. + +## Features + +### Credentials +Upon installing the app will ask for a username and password for both the web interface and the Soulseek network. If another Soulseek user already exists with that name, the app will still install correctly but won't login. A new username can be configured in the settings. + +### Secure access + +slskd runs as a daemon or Docker container in your network (or in the cloud!) and is accessible from a web browser. It's designed to be exposed to the internet, and everything is secured with a token that [you can control](https://github.com/slskd/slskd/blob/master/docs/config.md#authentication). + +![image](https://user-images.githubusercontent.com/17145758/193290217-0e6d87f5-a547-4451-8d90-d554a902716c.png) + +### Search + +Search for things just like you're used to with the official Soulseek client. slskd makes it easy to enter multiple searches quickly. + +![image](https://user-images.githubusercontent.com/17145758/193286989-30bd524d-81b6-4721-bd72-e4438c2b7b69.png) + +### Results + +Sort and filter search results using the same filters you use today. Dismiss results you're not interested in, and download the ones you want in a couple of clicks. + +![image](https://user-images.githubusercontent.com/17145758/193288396-dc3cc83d-6d93-414a-93f6-cea0696ac245.png) + +### Downloads + +Monitor the speed and status of downloads, grouped by user and folder. Click the progress bar to fetch your place in queue, and use the selection tools to cancel, retry, or clear completed downloads. Use the controls at the top to quickly manage downloads by status. + +![image](https://user-images.githubusercontent.com/17145758/193289840-3aee153f-3656-4f15-b086-8b1ca25d38bb.png) + +### Pretty much everything else + +slskd can do almost everything the official Soulseek client can; browse user shares, join chat rooms, privately chat with other users. + +New features are added all the time! \ No newline at end of file diff --git a/apps/slskd/metadata/logo.jpg b/apps/slskd/metadata/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aa6d51f75942de62e3969c9fe45ed4d0fdf3a39f GIT binary patch literal 9496 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 literal 0 HcmV?d00001