Skip to content
Draft
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
9 changes: 7 additions & 2 deletions cmd/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func genMissingThumbnails() *cobra.Command {
}
canGenerateWaveform := func(mimeType string) bool {
switch mimeType {
case "audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav":
case "audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav", "audio/m4a":
return true
default:
return false
Expand Down Expand Up @@ -245,6 +245,11 @@ func genMissingThumbnails() *cobra.Command {
if err != nil {
return fmt.Errorf("failed to generate thumbnail: %w", err)
}
case "audio/m4a":
r, err = ip.WaveformM4a(src, waveformWidth, waveformHeight)
if err != nil {
return fmt.Errorf("failed to generate thumbnail: %w", err)
}
default:
return nil
}
Expand Down Expand Up @@ -290,7 +295,7 @@ func genMissingThumbnails() *cobra.Command {
"AND f.mime IN ("+
// サムネイル生成が可能なmimeが変わったらここを変える
"'image/jpeg', 'image/png', 'image/gif', 'image/webp', "+
"'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav'"+
"'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav', 'audio/m4a'"+
") "+
"GROUP BY f.id, f.created_at "+
"HAVING COUNT(ft.file_id) = 0 "+
Expand Down
8 changes: 7 additions & 1 deletion service/file/manager_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (m *managerImpl) canGenerateThumbnail(mimeType string) bool {

func (m *managerImpl) canGenerateWaveform(mimeType string) bool {
switch mimeType {
case "audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav":
case "audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav", "audio/m4a":
return true
default:
return false
Expand Down Expand Up @@ -146,7 +146,13 @@ func (m *managerImpl) Save(ctx context.Context, args SaveArgs) (model.File, erro
if err != nil {
m.l.Warn("failed to generate thumbnail", zap.Error(err), zap.Stringer("fid", f.ID))
}
case "audio/m4a":
r, err = m.ip.WaveformM4a(src, waveformWidth, waveformHeight)
if err != nil {
m.l.Warn("failed to generate thumbnail", zap.Error(err), zap.Stringer("fid", f.ID))
}
}


if r != nil {
thumbnail := model.FileThumbnail{
Expand Down
63 changes: 63 additions & 0 deletions service/file/manager_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,69 @@ func TestManagerImpl_Save(t *testing.T) {
assert.EqualValues(t, "image/svg+xml", thumbs[0].Mime)
}
})
t.Run("audio with generating waveform (m4a, io.ReadSeeker)", func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
repo := mock_repository.NewMockFileRepository(ctrl)
fs := mock_storage.NewMockFileStorage(ctrl)
ip := mock_imaging.NewMockProcessor(ctrl)
fm := initFM(t, repo, fs, ip)

data := []byte("test text file")
hash := "7e6d5d7ae4965bfecc6d818f76eb832b"
args := SaveArgs{
FileName: "dummy.m4a",
FileSize: int64(len(data)),
MimeType: "audio/m4a",
FileType: model.FileTypeUserFile,
ChannelID: optional.From(uuid.NewV3(uuid.Nil, "c")),
Src: bytes.NewReader(data),
}
waveform := bytes.NewBufferString("dummy svg file")

fs.EXPECT().
SaveByKey(gomock.Any(), gomock.Any(), args.FileName, args.MimeType, args.FileType).
Do(func(src io.Reader, _, _, _ string, _ model.FileType) {
_, _ = io.Copy(io.Discard, src)
}).
Return(nil).
Times(1)
fs.EXPECT().
SaveByKey(gomock.Any(), gomock.Any(), gomock.Any(), "image/svg+xml", model.FileTypeThumbnail).
DoAndReturn(func(src io.Reader, _, _, _ string, _ model.FileType) error {
_, _ = io.Copy(io.Discard, src)
return nil
}).
Times(1)
repo.EXPECT().
SaveFileMeta(gomock.Any(), gomock.Any(), []*model.FileACLEntry{{UserID: uuid.Nil, Allow: true}}).
Do(func(_ context.Context, meta *model.FileMeta, _ []*model.FileACLEntry) { meta.CreatedAt = time.Now() }).
Return(nil).
Times(1)
ip.EXPECT().
WaveformM4a(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(src io.ReadSeeker, _, _ int) { _, _ = io.Copy(io.Discard, src) }).
Return(waveform, nil).
Times(1)

result, err := fm.Save(context.TODO(), args)
if assert.NoError(t, err) {
assert.NotEmpty(t, result.GetID())
assert.EqualValues(t, args.FileName, result.GetFileName())
assert.EqualValues(t, args.FileSize, result.GetFileSize())
assert.EqualValues(t, args.MimeType, result.GetMIMEType())
assert.EqualValues(t, args.FileType, result.GetFileType())
assert.EqualValues(t, args.ChannelID, result.GetUploadChannelID())
assert.EqualValues(t, args.CreatorID, result.GetCreatorID())
assert.EqualValues(t, hash, result.GetMD5Hash())
assert.EqualValues(t, false, result.IsAnimatedImage())
assert.NotEmpty(t, result.GetCreatedAt())
thumbs := result.GetThumbnails()
assert.EqualValues(t, 1, len(thumbs))
assert.EqualValues(t, model.ThumbnailTypeWaveform, thumbs[0].Type)
assert.EqualValues(t, "image/svg+xml", thumbs[0].Mime)
}
})
}

func TestManagerImpl_Get(t *testing.T) {
Expand Down
15 changes: 15 additions & 0 deletions service/imaging/mock_imaging/mock_processor.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/imaging/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type Processor interface {
FitAnimationGIF(src io.Reader, width, height int) (*bytes.Reader, error)
WaveformMp3(src io.ReadSeeker, width, height int) (io.Reader, error)
WaveformWav(src io.ReadSeeker, width, height int) (io.Reader, error)
WaveformM4a(src io.ReadSeeker, width, height int) (io.Reader, error)
}
8 changes: 8 additions & 0 deletions service/imaging/processor_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,11 @@
Height: height,
})
}
func (p *defaultProcessor) WaveformM4a(src io.ReadSeeker, width, height int) (io.Reader, error) {
d := m4a.NewDecoder(src)

Check failure on line 266 in service/imaging/processor_default.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: m4a

Check failure on line 266 in service/imaging/processor_default.go

View workflow job for this annotation

GitHub Actions / Build

undefined: m4a
return waveform.OutputWaveformImageM4a(d, &waveform.Option{

Check failure on line 267 in service/imaging/processor_default.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: waveform.OutputWaveformImageM4a (typecheck)

Check failure on line 267 in service/imaging/processor_default.go

View workflow job for this annotation

GitHub Actions / Build

undefined: waveform.OutputWaveformImageM4a
Resolution: width / 5,
Width: width,
Height: height,
})
}
33 changes: 31 additions & 2 deletions service/qall/soundboard_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
}

// checkAudioDuration は拡張子(ext)に基づいて対応ライブラリを使い、秒数をチェックする
// mp3 / wav / ogg に対応し、それ以外は "we only support mp3, wav, ogg" エラー
// mp3 / wav / ogg / m4a に対応し、それ以外は "we only support mp3, wav, ogg, m4a" エラー
func checkAudioDuration(fileBytes []byte, contentType string, maxSeconds float64) error {
switch contentType {
case "audio/mpeg", "audio/mp3":
Expand Down Expand Up @@ -147,9 +147,18 @@
return fmt.Errorf("audio is too long (%.1f sec). Must be <= %.0f", dur, maxSeconds)
}
return nil
case "audio/m4a":
dur, err := getM4aDuration(fileBytes)
if err != nil {
return fmt.Errorf("m4a decode error: %w", err)
}
if dur > maxSeconds {
return fmt.Errorf("audio is too long (%.1f sec). Must be <= %.0f", dur, maxSeconds)
}
return nil

default:
return errors.New("we only support .mp3, .wav, .ogg")
return errors.New("we only support .mp3, .wav, .ogg, .m4a")
}
}

Expand Down Expand Up @@ -206,3 +215,23 @@
seconds := sampleCount / sampleRate
return seconds, nil
}

// getM4aDuration returns duration in seconds for M4A
func getM4aDuration(data []byte) (float64, error) {
r := bytes.NewReader(data)
m4aDecoder := m4a.NewDecoder(r)

Check failure on line 222 in service/qall/soundboard_impl.go

View workflow job for this annotation

GitHub Actions / Build

undefined: m4a
buf, err := m4aDecoder.FullPCMBuffer()
if err != nil {
return 0, err
}
if buf == nil || buf.Format == nil {
return 0, errors.New("invalid m4a format or buffer")
}
sampleRate := float64(buf.Format.SampleRate)
sampleCount := float64(len(buf.Data)) // PCMBufferのサンプル数
if sampleRate <= 0 {
return 0, errors.New("invalid m4a sample rate")
}
seconds := sampleCount / sampleRate
return seconds, nil
}
Loading