Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,24 @@ RUN apk add --no-cache \
shadow \
su-exec

# Install ytmusicapi in the container
RUN pip install --no-cache-dir ytmusicapi
# SpotiFLAC version to install (FLAC download source). Installs from PyPI and puts
# the `spotiflac` CLI on PATH. Override at build time, e.g.:
# --build-arg SPOTIFLAC_VERSION=1.2.4
ARG SPOTIFLAC_VERSION=1.2.3

# Install ytmusicapi (youtube fallback search) and SpotiFLAC (FLAC download source).
# This provides both the `spotiflac` CLI (URL-based imports) and the importable
# SpotiFLAC module used by spotiflac_dl.py (ISRC/title-artist matching).
RUN pip install --no-cache-dir ytmusicapi "SpotiFLAC==${SPOTIFLAC_VERSION}"

# Set working directory
WORKDIR /opt/explo/

# Copy entrypoint, binary, python helper
# Copy entrypoint, binary, python helpers
COPY ./docker/start.sh /start.sh
COPY --from=builder /app/explo .
COPY src/downloader/youtube_music/search_ytmusic.py .
COPY src/downloader/spotiflac/spotiflac_dl.py .


RUN chmod +x /start.sh ./explo
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Explo uses the [ListenBrainz](https://listenbrainz.org/) recommendation engine t
- Apple Music
- ListenBrainz
- Spotify
- Request tracks from YouTube, Soulseek, or both
- Request tracks from YouTube, Soulseek, or SpotiFLAC (lossless FLAC)
- Add metadata to downloaded tracks
- Create playlists in your music system
- Keep previous playlists for later listening
Expand Down Expand Up @@ -55,6 +55,8 @@ Explo uses the following 3rd-party libraries:

- [ytmusicapi](https://github.com/sigma67/ytmusicapi): Unofficial Youtube Music API

- [SpotiFLAC](https://github.com/ShuShuzinhuu/SpotiFLAC-Module-Version): Lossless FLAC downloader (official CLI)

- [notify](https://github.com/nikoksr/notify): Module for sending notifications to different services

- [gocron](https://github.com/go-co-op/gocron): Internal cron scheduling
Expand Down
26 changes: 26 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ LIBRARY_NAME=
# Keep original file permissions when moving files (set to false on Synology devices)
# KEEP_PERMISSIONS=true
# Comma-separated list (no spaces) of download services, in priority order (default: youtube)
# Supported: youtube, slskd, spotiflac
# DOWNLOAD_SERVICES=youtube
# Path templating, Options are Artist, Album, TrackName, TrackNumber, File, Ext (eg. "{{Artist}}/{{Album}}/{{File}}")
# PATH_TEMPLATING=""
Expand Down Expand Up @@ -101,6 +102,31 @@ LIBRARY_NAME=
# Comma-separated (without spaces) keywords to avoid, when filtering slskd results (default: live,remix,instrumental,extended,clean,acapella)
# FILTER_LIST=live,remix,instrumental,extended,clean,acapella

# === SpotiFLAC Configuration ===

# Downloads lossless FLAC via SpotiFLAC (https://github.com/ShuShuzinhuu/SpotiFLAC-Module-Version),
# bundled in the docker image (pip install "SpotiFLAC>=1.2.3") with ffmpeg in $PATH.
# To build the image with a different version: --build-arg SPOTIFLAC_VERSION=<version>
# Add 'spotiflac' to DOWNLOAD_SERVICES to enable it. Tracks with a streaming URL
# (Spotify-imported playlists) use the official `spotiflac` CLI; tracks matched by
# ISRC/title-artist (e.g. ListenBrainz discovery) use the bundled module helper.
# Tracks SpotiFLAC can't source fall through to the other configured services.

# FLAC sources to try, in priority order (default: deezer,tidal,qobuz,amazon)
# SPOTIFLAC_SOURCES=deezer,tidal,qobuz,amazon
# Preferred quality for the CLI path (default: LOSSLESS; e.g. HI_RES_LOSSLESS, LOSSLESS, HIGH)
# SPOTIFLAC_QUALITY=LOSSLESS
# Path to the spotiflac CLI (default: spotiflac, resolved from $PATH)
# SPOTIFLAC_BIN=spotiflac
# Python interpreter with the SpotiFLAC module installed, for the helper path (default: python3)
# SPOTIFLAC_PYTHON_PATH=python3
# Path to the bundled module helper (default: spotiflac_dl.py; set an absolute path for the binary version)
# SPOTIFLAC_SCRIPT_PATH=spotiflac_dl.py
# Max seconds to spend downloading a single track before giving up (default: 180)
# SPOTIFLAC_TIMEOUT=180
# Extra download attempts per track on failure, cycling all sources (default: 2)
# SPOTIFLAC_RETRIES=2

# === Metadata / Formatting ===

# Set to true to merge featured artists into title (recommended), false appends them to artist field (default: true)
Expand Down
17 changes: 17 additions & 0 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type DownloadConfig struct {
Youtube Youtube
YoutubeMusic YoutubeMusic
Slskd Slskd
Spotiflac Spotiflac
ExcludeLocal bool
DownloadLimiter int `env:"DOWNLOAD_LIMITER" env-default:"1"` // rate limit download operations
OverwriteMetadata bool `env:"OVERWRITE_METADATA" env-default:"false"` // overwrite metadata when migrating downloads
Expand Down Expand Up @@ -134,6 +135,22 @@ type YoutubeMusic struct {
Filters Filters
}

// Spotiflac configures the 'spotiflac' download service. It has two paths:
// tracks that carry a streaming URL (e.g. Spotify imports) are downloaded via the
// official SpotiFLAC CLI (BinPath); tracks matched by ISRC/title-artist (e.g.
// ListenBrainz discovery) are downloaded via the bundled module helper
// (PythonPath + ScriptPath). Sources is the list of FLAC providers to try in
// priority order; see sample.env for the full reference.
type Spotiflac struct {
Sources []string `env:"SPOTIFLAC_SOURCES" env-default:"deezer,tidal,qobuz,amazon"`
Quality string `env:"SPOTIFLAC_QUALITY" env-default:"LOSSLESS"`
BinPath string `env:"SPOTIFLAC_BIN" env-default:"spotiflac"`
PythonPath string `env:"SPOTIFLAC_PYTHON_PATH" env-default:"python3"`
ScriptPath string `env:"SPOTIFLAC_SCRIPT_PATH" env-default:"spotiflac_dl.py"`
Timeout int `env:"SPOTIFLAC_TIMEOUT" env-default:"180"`
Retries int `env:"SPOTIFLAC_RETRIES" env-default:"2"`
}

type Slskd struct {
APIKey string `env:"SLSKD_API_KEY"`
URL string `env:"SLSKD_URL"`
Expand Down
4 changes: 3 additions & 1 deletion src/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func NewDownloader(cfg *cfg.DownloadConfig, httpClient *util.HttpClient, filterL
slskdClient := NewSlskd(cfg.Slskd, cfg.DownloadDir)
slskdClient.AddHeader()
downloader = append(downloader, slskdClient)
case "spotiflac":
downloader = append(downloader, NewSpotiflac(cfg.Spotiflac, cfg.DownloadDir))
default:
return nil, fmt.Errorf("downloader '%s' not supported", service)
}
Expand Down Expand Up @@ -120,7 +122,7 @@ func (c *DownloadClient) StartDownload(tracks *[]*models.Track) {
}
func (c *DownloadClient) needsDownloadDir() bool {
for _, svc := range c.Cfg.Services {
if svc == "youtube" || svc == "youtube-music" {
if svc == "youtube" || svc == "youtube-music" || svc == "spotiflac" {
return true
}
}
Expand Down
Loading