Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@ bin/*
!bin/.keep

music/

venv/
7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ RUN apk update && apk add -U make
RUN make build


FROM python:3.12-alpine
FROM alpine:3.20

WORKDIR /app

COPY --from=build /app/bin/audio-scraper /app/audio-scraper
COPY ./scripts /app/scripts

RUN apk update && \
apk add -U yt-dlp-core ffmpeg

RUN pip install ytmusicapi eyed3
apk add -U yt-dlp ffmpeg

EXPOSE 8080

Expand Down
32 changes: 14 additions & 18 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import (

"github.com/gorilla/mux"

filesystemimpl "audio-scraper/internal/adapters/filesystem/impl"
itunesimpl "audio-scraper/internal/adapters/itunes/impl"
lrclibimpl "audio-scraper/internal/adapters/lrclib/impl"
storeimpl "audio-scraper/internal/adapters/store/impl"
youtubeimpl "audio-scraper/internal/adapters/youtube/impl"
"audio-scraper/internal/api"
"audio-scraper/internal/constants"
"audio-scraper/internal/logger"
"audio-scraper/internal/pool"
"audio-scraper/internal/providers/filesystem"
"audio-scraper/internal/providers/lrclib"
"audio-scraper/internal/providers/spotify"
"audio-scraper/internal/providers/store"
"audio-scraper/internal/providers/youtube"
)

func main() {
Expand All @@ -29,15 +29,11 @@ func main() {
}
log.Info("started server", "host", "0.0.0.0", "port", port)

sp, err := spotify.NewSpotifyProvider(os.Getenv("SPOTIFY_CLIENT_ID"), os.Getenv("SPOTIFY_CLIENT_SECRET"))
if err != nil {
log.Error("failed to initialize Spotify provider", "error", err)
return
}
st := store.NewStoreProvider(log)
yt := youtube.NewYTProvider()
lrc := lrclib.NewLrclibProvider()
fs, err := filesystem.NewFSProvider(os.Getenv("MUSIC_HOME"), lrc)
md := itunesimpl.New()
st := storeimpl.New(log)
yt := youtubeimpl.New()
lrc := lrclibimpl.New()
fs, err := filesystemimpl.New(os.Getenv("MUSIC_HOME"), lrc)
if err != nil {
log.Error("failed to initialize filesystem provider", "error", err)
return
Expand All @@ -54,10 +50,10 @@ func main() {
FS: fs,
})
h := api.NewHandlers(&api.Deps{
Log: log,
Spotify: sp,
Store: st,
Queue: q,
Log: log,
Metadata: md,
Store: st,
Queue: q,
})
router := mux.NewRouter()
router.HandleFunc("/", h.HealthHandler).Methods("GET")
Expand Down
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ require (
github.com/faiface/beep v1.1.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/zmb3/spotify/v2 v2.4.3
golang.org/x/oauth2 v0.33.0
)

require (
Expand Down
378 changes: 0 additions & 378 deletions go.sum

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package filesystem
// Package filesystemimpl implements the filesystem.Provider port: it writes
// downloaded audio into a MUSIC_HOME/Artist/Album tree and tags the files.
package filesystemimpl

import (
"context"
Expand All @@ -14,27 +16,33 @@ import (

"github.com/bogem/id3v2/v2"

"audio-scraper/internal/adapters/filesystem"
"audio-scraper/internal/adapters/lrclib"
"audio-scraper/internal/logger"
"audio-scraper/internal/models"
"audio-scraper/internal/providers/lrclib"
)

type FSClient struct {
// Client is the local-filesystem library provider.
type Client struct {
musicHome string
lrc *lrclib.LrclibClient
lrc lrclib.Provider
}

func NewFSProvider(musicHome string, lrc *lrclib.LrclibClient) (*FSClient, error) {
var _ filesystem.Provider = (*Client)(nil)

// New returns a filesystem provider rooted at musicHome, using lrc to resolve
// lyrics during tagging.
func New(musicHome string, lrc lrclib.Provider) (*Client, error) {
if musicHome == "" {
return nil, errors.New("missing MUSIC_HOME")
}
return &FSClient{
return &Client{
musicHome: musicHome,
lrc: lrc,
}, nil
}

func (f *FSClient) InitializePath(ctx context.Context, job *models.DownloadJob) (string, error) {
func (f *Client) InitializePath(ctx context.Context, job *models.DownloadJob) (string, error) {
log := logger.From(ctx)
path := filepath.Join(
f.musicHome,
Expand Down Expand Up @@ -63,7 +71,7 @@ func (f *FSClient) InitializePath(ctx context.Context, job *models.DownloadJob)
return outputPath, nil
}

func (f *FSClient) TagFile(ctx context.Context, filePath string, job *models.DownloadJob) error {
func (f *Client) TagFile(ctx context.Context, filePath string, job *models.DownloadJob) error {
log := logger.From(ctx)
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
if err != nil {
Expand Down
19 changes: 19 additions & 0 deletions internal/adapters/filesystem/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Package filesystem defines the local library port: where downloaded audio is
// placed on disk and how it is tagged. The concrete implementation lives in the
// impl subpackage.
package filesystem

import (
"context"

"audio-scraper/internal/models"
)

// Provider manages on-disk placement and tagging of downloaded tracks.
type Provider interface {
// InitializePath creates the destination directory tree and returns the
// output file path for the job.
InitializePath(ctx context.Context, job *models.DownloadJob) (string, error)
// TagFile writes ID3 metadata, cover art and lyrics to the file at filePath.
TagFile(ctx context.Context, filePath string, job *models.DownloadJob) error
}
31 changes: 31 additions & 0 deletions internal/adapters/filesystem/mocks/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Package filesystemmock provides a test double for filesystem.Provider.
package filesystemmock

import (
"context"

"audio-scraper/internal/adapters/filesystem"
"audio-scraper/internal/models"
)

// Mock implements filesystem.Provider; set the *Func fields to control behavior.
type Mock struct {
InitializePathFunc func(ctx context.Context, job *models.DownloadJob) (string, error)
TagFileFunc func(ctx context.Context, filePath string, job *models.DownloadJob) error
}

var _ filesystem.Provider = (*Mock)(nil)

func (m *Mock) InitializePath(ctx context.Context, job *models.DownloadJob) (string, error) {
if m.InitializePathFunc != nil {
return m.InitializePathFunc(ctx, job)
}
return "", nil
}

func (m *Mock) TagFile(ctx context.Context, filePath string, job *models.DownloadJob) error {
if m.TagFileFunc != nil {
return m.TagFileFunc(ctx, filePath, job)
}
return nil
}
Loading
Loading