commit d38623c4fe9aba78289994f8964132d61e22e5d1 Author: Ramon Rüttimann Date: Wed Jan 1 15:52:16 2020 +0100 new initial commit Signed-off-by: Ramon Rüttimann diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..cedac86 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,46 @@ +kind: pipeline +name: default + +steps: + - name: test + image: golang + environment: + GO111MODULE: on + commands: + - echo $GO111MODULE + - go test ./... -v -cover + + - name: build + image: golang + environment: + GO111MODULE: on + commands: + - go build + + - name: docker + image: plugins/docker + settings: + auto_tag: true + build_args_from_env: + - DRONE_COMMIT + - DRONE_TAG + username: + from_secret: username + password: + from_secret: password + repo: registry.ramonr.ch/mediaconverter + registry: registry.ramonr.ch + when: + event: tag +--- +kind: secret +name: username +get: + path: registry-basicauth + name: user +--- +kind: secret +name: password +get: + path: registry-basicauth + name: password diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5aeceaa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:alpine as builder + +ARG DRONE_COMMIT="none" +ARG DRONE_TAG="none" + +RUN apk --no-cache add git +WORKDIR /app +COPY . . +# ldflags to remove debugging tables +ENV DRONE_COMMIT=$DRONE_COMMIT +ENV DRONE_TAG=$DRONE_TAG +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s -X main.version=$DRONE_TAG -X main.commit=$DRONE_COMMIT" -o "mediaconverter" . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s -X main.version=$DRONE_TAG -X main.commit=$DRONE_COMMIT" -o "faker" ./faker/ + +FROM alfg/ffmpeg +USER 1000:1000 +COPY --from=builder /app/mediaconverter /mediaconverter +COPY --from=builder /app/faker /mediaconverter + +CMD ["/mediaconverter"] diff --git a/converter/converter.go b/converter/converter.go new file mode 100644 index 0000000..f0085af --- /dev/null +++ b/converter/converter.go @@ -0,0 +1,195 @@ +package converter + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/xfrr/goffmpeg/transcoder" + "k8s.io/klog" +) + +type Converter struct { + *message + // albumFolder is populated at runtime and contains the album name + albumFolder string + // the "automatically add to iTunes.localized" directory + iTunesDir string +} + +// CreateMessage is basically the reverse process to the conversion. +// instead of getting the message and using it, this function can +// read a directory and create the message from it. +// Only populates what is necessary to do the conversion. +// artist must match the directory name. +func CreateMessage(artistName, albumFolder string) ([]byte, error) { + albumName := path.Base(albumFolder) + artistPath := path.Clean(strings.ReplaceAll(albumFolder, albumName, "")) + var trackFiles []trackFile + err := filepath.Walk(albumFolder, func(path string, info os.FileInfo, err error) error { + if err != nil { + return errors.Wrapf(err, "error visiting file") + } + if filepath.Ext(path) == ".flac" { + trackFiles = append(trackFiles, trackFile{Path: path}) + } + return nil + }) + if err != nil { + return nil, errors.Wrapf(err, "error walking path") + } + sort.Slice(trackFiles, func(i, j int) bool { + return trackFiles[i].Path < trackFiles[j].Path + }) + + tracks := func() []track { + var tracks []track + for i, file := range trackFiles { + tracks = append( + tracks, + track{ + Title: strings.ReplaceAll(path.Base(file.Path), ".flac", ""), + TrackNumber: strconv.Itoa(i + 1), + }, + ) + } + return tracks + }() + + m := message{ + Tracks: tracks, + TrackFiles: trackFiles, + EventType: "Download", + Artist: artist{ + Name: artistName, + Path: artistPath, + }, + } + return json.Marshal(m) +} + +type message struct { + Tracks []track `json:"tracks"` + TrackFiles []trackFile `json:"trackFiles"` + IsUpgrade bool `json:"isUpgrade"` + EventType string `json:"eventType"` + Artist artist `json:"artist"` +} + +type artist struct { + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + MBID string `json:"mbID"` +} + +type track struct { + ID int `json:"id"` + Title string `json:"title"` + TrackNumber string `json:"trackNumber"` + Quality string `json:"quality"` + QualityVersion int `json:"qualityVersion"` +} + +func (t *track) mp3Name() string { + return fmt.Sprintf("%s - %s.mp3", t.TrackNumber, t.Title) +} + +type trackFile struct { + ID int `json:"id"` + Path string `json:"path"` + Quality string `json:"quality"` + QualityVersion int `json:"qualityVersion"` + SceneName string `json:"sceneName"` +} + +// New creates a new converter +func New(iTunesDir string) *Converter { + return &Converter{ + iTunesDir: iTunesDir, + } +} + +func (tf *trackFile) Convert(newFile string) error { + trans := new(transcoder.Transcoder) + if err := trans.Initialize(tf.Path, newFile); err != nil { + return err + } + trans.MediaFile().SetAudioVariableBitrate() + trans.MediaFile().SetAudioBitRate("0") + return <-trans.Run(false) +} + +// convertFiles converts all files from the message into the +// iTunesDir and changes the permissions. +func (c *Converter) convertFiles() error { + for i, track := range c.Tracks { + destination := path.Join(c.iTunesDir, c.albumFolder, track.mp3Name()) + if err := c.TrackFiles[i].Convert(destination); err != nil { + return errors.Wrapf(err, "could not convert %v", c.TrackFiles[i].Path) + } + + if err := changePermission(path.Join(destination)); err != nil { + return errors.Wrapf(err, "could not change permissions on %v", destination) + } + } + return nil +} + +func (m *message) extractAlbumFolder() string { + artistDir := filepath.Clean(m.Artist.Path) + "/" + fileDir := filepath.Dir(m.TrackFiles[0].Path) + return strings.ReplaceAll(fileDir, artistDir, "") +} + +// Process the message, converting the files, changing permissions and copying artwork +func (c *Converter) Process(msgSource io.Reader) error { + // read and parse the message + encMessage, err := ioutil.ReadAll(msgSource) + if err != nil { + return errors.Wrapf(err, "could not read given source") + } + + c.message = new(message) + if err := json.Unmarshal(encMessage, c.message); err != nil { + return errors.Wrapf(err, "could not unmarshal given source") + } + + if c.message.EventType != "Download" { + klog.Infof("eventType is %v, nothing to do", c.message.EventType) + } + + c.albumFolder = c.TrackFiles[0].SceneName + if c.albumFolder == "" { + c.albumFolder = c.extractAlbumFolder() + } + klog.Infof("got request to transcode %v", c.albumFolder) + + // actual conversion work + if err := os.MkdirAll(path.Join(c.iTunesDir, c.albumFolder), os.ModePerm); err != nil { + return errors.Wrapf(err, "could not create iTunes Directory") + } + if err := os.Chmod(path.Join(c.iTunesDir, c.albumFolder), 0775); err != nil { + return errors.Wrapf(err, "could not change permissions on iTunes dir") + } + + if err := c.convertFiles(); err != nil { + return errors.Wrapf(err, "could not convert files") + } + + klog.Infof("Successfully converted %v", path.Join(c.Artist.Path, c.albumFolder)) + return nil +} + +func changePermission(newFile string) error { + err := os.Chmod(newFile, 0777) + return errors.Wrapf(err, "chmod failed") +} diff --git a/converter/converter_test.go b/converter/converter_test.go new file mode 100644 index 0000000..f958bda --- /dev/null +++ b/converter/converter_test.go @@ -0,0 +1,34 @@ +package converter + +import "testing" + +func TestExtractAlbumFolder(t *testing.T) { + tests := []struct { + m *message + albumFolder string + }{ + {createMessage("/test/artistX", "/test/artistX/albumY/songX.flac"), "albumY"}, + {createMessage("/test/artistX/", "/test/artistX/album Y/songX.flac"), "album Y"}, + } + + for _, tt := range tests { + actual := tt.m.extractAlbumFolder() + expected := tt.albumFolder + if actual != expected { + t.Errorf("expected %v, got %v", expected, actual) + } + } +} + +func createMessage(artistPath, filePath string) *message { + return &message{ + Artist: artist{ + Path: artistPath, + }, + TrackFiles: []trackFile{ + { + Path: filePath, + }, + }, + } +} diff --git a/faker/main.go b/faker/main.go new file mode 100644 index 0000000..8ad4819 --- /dev/null +++ b/faker/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "net/http" + + "git.ramonr.ch/ramon/mediaconverter/converter" + "k8s.io/klog" +) + +// set at compile time +var ( + version = "" + commit = "" +) + +func main() { + klog.InitFlags(nil) + postAddr := flag.String("post-addr", "mediaconverter", "the address where to post the request") + directory := flag.String("dir", "", "the directory that contains the flac files") + artistName := flag.String("artist", "", "name of the artist") + flag.Parse() + fmt.Printf("Mediaconverter version %v, commit %v\n", version, commit) + + msg, err := converter.CreateMessage(*artistName, *directory) + if err != nil { + klog.Fatalf("could not create message: %v", err) + } + + klog.Infof("sending request") + req, err := http.NewRequest("POST", *postAddr, bytes.NewBuffer(msg)) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + klog.Infof("received answer") + + fmt.Println("response Status:", resp.Status) + fmt.Println("response Headers:", resp.Header) + body, _ := ioutil.ReadAll(resp.Body) + fmt.Println("response Body:", string(body)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f4503ce --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.ramonr.ch/ramon/mediaconverter + +go 1.13 + +require ( + github.com/pkg/errors v0.8.1 + github.com/xfrr/goffmpeg v0.0.0-20191108092855-c49093f2d9f7 + k8s.io/klog v1.0.0 +) + +replace github.com/xfrr/goffmpeg => github.com/tommyknows/goffmpeg v0.0.0-20191109121430-95bbe3aa58b5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e8d6e68 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/tommyknows/goffmpeg v0.0.0-20191108092855-c49093f2d9f7 h1:19vePvbw3O7GvNDnipom3Fu8uLCN2XAZlpABr/Z+yFo= +github.com/tommyknows/goffmpeg v0.0.0-20191108092855-c49093f2d9f7/go.mod h1:W1AEwoY9XJ1POFmGvJ5y/sifvS6MnkGfwaCbV6nWKfo= +github.com/tommyknows/goffmpeg v0.0.0-20191109114406-40d09fee25ad h1:2ziJ/9WFv0zyZZvn2W7V/2SohYKw1M7K+9pHsGzW2bw= +github.com/tommyknows/goffmpeg v0.0.0-20191109114406-40d09fee25ad/go.mod h1:W1AEwoY9XJ1POFmGvJ5y/sifvS6MnkGfwaCbV6nWKfo= +github.com/tommyknows/goffmpeg v0.0.0-20191109120730-72121122651e h1:fuzOvyuRh3eGf+tgf7npiGQkg5Xn74DtlIMKizJpJNE= +github.com/tommyknows/goffmpeg v0.0.0-20191109120730-72121122651e/go.mod h1:W1AEwoY9XJ1POFmGvJ5y/sifvS6MnkGfwaCbV6nWKfo= +github.com/tommyknows/goffmpeg v0.0.0-20191109121430-95bbe3aa58b5 h1:UqjC/7jF5Sc9DhepBZyobGwNPiMNNtB2h+U6pHSLoEg= +github.com/tommyknows/goffmpeg v0.0.0-20191109121430-95bbe3aa58b5/go.mod h1:W1AEwoY9XJ1POFmGvJ5y/sifvS6MnkGfwaCbV6nWKfo= +github.com/xfrr/goffmpeg v0.0.0-20191108092855-c49093f2d9f7 h1:EMf+JgASgRmdgyXxsd5YqnDkmO6PI9cyp/yN7Z/wtk0= +github.com/xfrr/goffmpeg v0.0.0-20191108092855-c49093f2d9f7/go.mod h1:mL+qPvJWwu9An5a66+HyEJ7X7iEUSI09zX/Xl67XRXs= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fa0ac73 --- /dev/null +++ b/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + + "git.ramonr.ch/ramon/mediaconverter/converter" + "k8s.io/klog" +) + +const iTunesDir = "/mnt/data/media/iTunes/iTunes Media/Automatically Add to Music.localized" + +// set at compile time +var ( + version = "" + commit = "" +) + +func main() { + klog.InitFlags(nil) + listenAddr := flag.String("listen-addr", "127.0.0.1:8088", "the address where to listen on") + flag.Parse() + fmt.Printf("Mediaconverter version %v, commit %v\n", version, commit) + + http.HandleFunc("/", conversionHandler()) + fmt.Println("Starting to listen on", *listenAddr) + err := http.ListenAndServe(*listenAddr, nil) + fmt.Println(err) +} + +func conversionHandler() http.HandlerFunc { + conv := converter.New(iTunesDir) + + return func(w http.ResponseWriter, r *http.Request) { + if err := conv.Process(r.Body); err != nil { + klog.Errorf("could not convert: %v", err) + fmt.Fprintf(w, "could not convert: %v", err) + } + } +}