commit
d38623c4fe
@ -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
|
@ -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"]
|
@ -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")
|
||||
}
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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
|
@ -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=
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue