Compare commits
10 commits
4c5c46a5f2
...
ed7e1ddade
| Author | SHA1 | Date | |
|---|---|---|---|
| ed7e1ddade | |||
| 7a198a0273 | |||
| 5881f3d538 | |||
| 739bed214c | |||
| 0aa0ac284e | |||
| 0a55d514fc | |||
| 4990b09f67 | |||
| 04274dc788 | |||
| 38202e58aa | |||
| c87dc5e04f |
24 changed files with 598 additions and 49 deletions
7
LICENSE
7
LICENSE
|
|
@ -0,0 +1,7 @@
|
|||
© 2023 Dan Jones <danjones@goodevilgenius.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
23
README.md
Normal file
23
README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# strip-beats
|
||||
|
||||
Imagine you have a folder full of music videos. You want to go through those videos, turn it into an audio file, tag them, and add it to your music collection.
|
||||
|
||||
This app aims to streamline that.
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
- ffmpeg
|
||||
- fpcalc (for fingerprinting audio so that we can identify it and automatically pull tags)
|
||||
- mpv (for watching the video to ensure you're stipping out non-music elements, like an intro or outro)
|
||||
- An account on acousticid.org (for querying the fingerprint to get data)
|
||||
|
||||
## Install
|
||||
|
||||
- Pull the repo
|
||||
- `go install .`
|
||||
|
||||
Hopefully, I'll have binaries downloadable soon.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This does not work yet! Some features are implemented, but most are not yet. Have a look around at the code, but don't actually use this at all.
|
||||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/files"
|
||||
"codeberg.org/danjones000/strip-beats/input/boolean"
|
||||
"codeberg.org/danjones000/strip-beats/io/boolean"
|
||||
"codeberg.org/danjones000/strip-beats/media"
|
||||
"codeberg.org/danjones000/strip-beats/utils"
|
||||
"github.com/rkoesters/xdg/trash"
|
||||
|
|
@ -20,8 +20,10 @@ func PickNewFile() media.Probe {
|
|||
}
|
||||
|
||||
func SetFile(path string) media.Probe {
|
||||
resetTags()
|
||||
f := media.ProbeFile(path)
|
||||
file = &f
|
||||
copyTagsFromFile()
|
||||
return f
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +72,7 @@ func PickAgain() {
|
|||
}
|
||||
file = nil
|
||||
tmpfile = nil
|
||||
resetTags()
|
||||
}
|
||||
|
||||
func Finish() {
|
||||
|
|
|
|||
16
app/convert.go
Normal file
16
app/convert.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/media"
|
||||
"codeberg.org/danjones000/strip-beats/utils"
|
||||
)
|
||||
|
||||
func convert() {
|
||||
in := utils.Tern(tmpfile == nil, file, tmpfile)
|
||||
out, _ := media.ConvertAndTag(*in, tags)
|
||||
fmt.Println(out)
|
||||
|
||||
quit()
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package app
|
|||
|
||||
import (
|
||||
"os"
|
||||
p "path/filepath"
|
||||
s "strings"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/media"
|
||||
"codeberg.org/danjones000/strip-beats/utils"
|
||||
|
|
@ -31,7 +33,10 @@ func validateNumber(input string, lastChar rune) bool {
|
|||
}
|
||||
|
||||
func fadeFile() error {
|
||||
tmp, err := os.CreateTemp("", "audio.*.mka")
|
||||
base := p.Base(file.Format.Path)
|
||||
ext := p.Ext(base)
|
||||
base = s.TrimSuffix(base, ext)
|
||||
tmp, err := os.CreateTemp("", base+".*.mka")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
13
app/menu.go
13
app/menu.go
|
|
@ -3,14 +3,14 @@ package app
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/input/list"
|
||||
"codeberg.org/danjones000/strip-beats/io/list"
|
||||
)
|
||||
|
||||
func (st AppStep) Title() string {
|
||||
mustpick := "You need to pick a file"
|
||||
switch st {
|
||||
case Pick:
|
||||
return "Pick a new show"
|
||||
return "Pick a new file"
|
||||
case Watch:
|
||||
if file == nil {
|
||||
return mustpick
|
||||
|
|
@ -26,6 +26,11 @@ func (st AppStep) Title() string {
|
|||
return mustpick
|
||||
}
|
||||
return fmt.Sprintf("Should we try to identify %s", file.ShortPath())
|
||||
case Convert:
|
||||
if file == nil {
|
||||
return mustpick
|
||||
}
|
||||
return fmt.Sprintf("Convert %s?", file.ShortPath())
|
||||
case Restart:
|
||||
return "Forget current selection"
|
||||
case Quit:
|
||||
|
|
@ -54,6 +59,8 @@ func (st AppStep) Rune() rune {
|
|||
return 'r'
|
||||
case Print:
|
||||
return 'a'
|
||||
case Convert:
|
||||
return 'c'
|
||||
case Quit:
|
||||
return 'q'
|
||||
default:
|
||||
|
|
@ -70,7 +77,7 @@ func mainMenu() AppStep {
|
|||
if file == nil {
|
||||
steps = []list.Option{Pick, Quit}
|
||||
} else {
|
||||
steps = []list.Option{Pick, Watch, Fade, Print, Quit}
|
||||
steps = []list.Option{Pick, Watch, Fade, Print, Convert, Quit}
|
||||
}
|
||||
|
||||
step := list.List("What would you like to do next?", steps, nil)
|
||||
|
|
|
|||
164
app/print.go
164
app/print.go
|
|
@ -3,15 +3,48 @@ package app
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/io/boolean"
|
||||
"codeberg.org/danjones000/strip-beats/io/list"
|
||||
m "codeberg.org/danjones000/strip-beats/io/message"
|
||||
"codeberg.org/danjones000/strip-beats/media"
|
||||
"codeberg.org/danjones000/strip-beats/media/brainz"
|
||||
"codeberg.org/danjones000/strip-beats/utils"
|
||||
"github.com/akrennmair/slice"
|
||||
)
|
||||
|
||||
type recOpt struct {
|
||||
rec brainz.Recording
|
||||
r rune
|
||||
score float64
|
||||
}
|
||||
|
||||
func (o recOpt) Title() string {
|
||||
return o.rec.Title
|
||||
}
|
||||
|
||||
func (o recOpt) Text() string {
|
||||
return fmt.Sprintf(
|
||||
"(Score %.2f) By %s - First Released %s - %s %s",
|
||||
100*o.score,
|
||||
o.rec.FirstArtist().Name,
|
||||
o.rec.FirstReleaseDate,
|
||||
o.rec.FirstGenre().Name,
|
||||
utils.Tern(o.rec.Video, "(Video)", ""))
|
||||
}
|
||||
|
||||
func (o recOpt) Rune() rune {
|
||||
return o.r
|
||||
}
|
||||
|
||||
func (o recOpt) Selected() func() {
|
||||
return nil
|
||||
}
|
||||
|
||||
func print() {
|
||||
if file == nil {
|
||||
PickFileWithConf()
|
||||
}
|
||||
fp, err := media.Fingerprint("/home/drj/MyFiles/Videos/WebShows/YouTube/Dolly_Parton_-_Topic/Just_Because_I_m_a_Woman.Dolly_Parton_-_Topic.Fmv-XQerVkM.webm")
|
||||
fp, err := media.Fingerprint(file.Format.Path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -19,17 +52,136 @@ func print() {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var recs []brainz.Recording
|
||||
rec, ok := chooseRec(ids)
|
||||
if !ok {
|
||||
m.Message("Couldn't find a marching recording")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(rec.Title)
|
||||
|
||||
tags.Title = rec.Title
|
||||
|
||||
tags.MusicbrainzRecordingId = rec.Id
|
||||
tags.Date = rec.FirstReleaseDate
|
||||
tags.Genre = rec.FirstGenre().Name
|
||||
tags.Artist = rec.FirstArtist().Name
|
||||
tags.ArtistSort = rec.FirstArtist().SortName
|
||||
tags.AcoustidId = rec.AcousticId
|
||||
|
||||
rel := findFirstRelease(rec)
|
||||
if !boolean.Choose(fmt.Sprintf("Is %s album (%s %s) correct?", rel.Title, rel.Country, rel.Date)) {
|
||||
rel = chooseRel(rec)
|
||||
}
|
||||
media := rel.Media[0]
|
||||
track := media.Tracks[0]
|
||||
full, err := brainz.GetReleaseWithMedia(rel.Id)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
albumArtist := full.ArtistCredit[0]
|
||||
|
||||
tags.MusicbrainzReleaseGroupId = full.ReleaseGroup.Id
|
||||
tags.MusicbrainzAlbumId = rel.Id
|
||||
tags.MusicbrainzAlbumArtistId = albumArtist.Artist.Id
|
||||
tags.AlbumArtist = albumArtist.Name
|
||||
tags.Album = rel.Title
|
||||
if rel.Date != "" {
|
||||
tags.Date = rel.Date
|
||||
}
|
||||
tags.AlbumArtistSort = albumArtist.Artist.SortName
|
||||
tags.ReleaseCountry = rel.Country
|
||||
if len(full.LabelInfo) > 0 {
|
||||
tags.Label = full.LabelInfo[0].Label.Name
|
||||
tags.MusicBrainzLabelId = full.LabelInfo[0].Label.Id
|
||||
}
|
||||
if len(rel.Genres) > 0 && rel.Genres[0].Name != "" {
|
||||
tags.Genre = rel.Genres[0].Name
|
||||
}
|
||||
tags.Disc = media.Position
|
||||
tags.DiscCount = len(full.Media)
|
||||
tags.Track = track.Position
|
||||
tags.TrackCount = media.TrackCount
|
||||
}
|
||||
|
||||
func chooseRec(ids media.IdResults) (brainz.Recording, bool) {
|
||||
var recs []list.Option
|
||||
var rec brainz.Recording
|
||||
var err error
|
||||
i := 'a'
|
||||
for _, res := range ids.Results {
|
||||
for _, rec := range res.Recordings {
|
||||
for _, rec = range res.Recordings {
|
||||
err = brainz.FillRecording(&rec)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
recs = append(recs, rec)
|
||||
|
||||
rec.AcousticId = res.Id
|
||||
recs = append(recs, recOpt{rec, i, res.Score})
|
||||
if rec.Title != "" {
|
||||
// Empty titles will be filtered out, so we don't need to increment them
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
recs = slice.Filter(recs, func(opt list.Option) bool {
|
||||
return opt.Title() != ""
|
||||
})
|
||||
|
||||
fmt.Printf("%+v\n", recs)
|
||||
if len(recs) < 1 {
|
||||
return rec, false
|
||||
}
|
||||
|
||||
return list.List("Which recording is the correct one?", recs, nil).(recOpt).rec, true
|
||||
}
|
||||
|
||||
func findFirstRelease(rec brainz.Recording) brainz.Release {
|
||||
var rel brainz.Release
|
||||
for _, rel = range rec.Releases {
|
||||
if rel.Date == rec.FirstReleaseDate {
|
||||
return rel
|
||||
}
|
||||
}
|
||||
if len(rec.Releases) > 0 {
|
||||
return rec.Releases[0]
|
||||
}
|
||||
return brainz.Release{}
|
||||
}
|
||||
|
||||
func chooseRel(rec brainz.Recording) brainz.Release {
|
||||
var rels []list.Option
|
||||
var rel brainz.Release
|
||||
i := 'a'
|
||||
for _, rel = range rec.Releases {
|
||||
rels = append(rels, relOpt{rel, i})
|
||||
if rel.Title != "" {
|
||||
// Empty titles will be filtered out, so we don't need to increment them
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
rels = slice.Filter(rels, func(opt list.Option) bool {
|
||||
return opt.Title() != ""
|
||||
})
|
||||
|
||||
return list.List("Which releases is the correct one?", rels, nil).(relOpt).rel
|
||||
}
|
||||
|
||||
type relOpt struct {
|
||||
rel brainz.Release
|
||||
r rune
|
||||
}
|
||||
|
||||
func (o relOpt) Title() string {
|
||||
return o.rel.Title
|
||||
}
|
||||
|
||||
func (o relOpt) Text() string {
|
||||
return fmt.Sprintf("%s %s", o.rel.Country, o.rel.Date)
|
||||
}
|
||||
|
||||
func (o relOpt) Rune() rune {
|
||||
return o.r
|
||||
}
|
||||
|
||||
func (o relOpt) Selected() func() {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
13
app/run.go
13
app/run.go
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/input/boolean"
|
||||
"codeberg.org/danjones000/strip-beats/io/boolean"
|
||||
"codeberg.org/danjones000/strip-beats/media"
|
||||
"codeberg.org/danjones000/strip-beats/media/brainz"
|
||||
)
|
||||
|
|
@ -16,6 +16,7 @@ const (
|
|||
Watch
|
||||
Fade
|
||||
Print
|
||||
Convert
|
||||
Restart
|
||||
Quit
|
||||
)
|
||||
|
|
@ -51,14 +52,17 @@ func testMb() {
|
|||
}
|
||||
|
||||
func testPrint() {
|
||||
SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Dolly_Parton_-_Topic/Just_Because_I_m_a_Woman.Dolly_Parton_-_Topic.Fmv-XQerVkM.webm")
|
||||
// SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Dolly_Parton_-_Topic/Just_Because_I_m_a_Woman.Dolly_Parton_-_Topic.Fmv-XQerVkM.webm")
|
||||
// SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Whitney_Houston/I_Will_Always_Love_You_Ultimate_Collection_Edit.Whitney_Houston.rB7z_l8mBxw.mp4")
|
||||
SetFile("/home/drj/MyFiles/Music/Original_Broadway_Cast_of_Hamilton/Hamilton/d02t12-We_Know.m4a")
|
||||
// SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/KaceyMusgravesVEVO/Kacey_Musgraves_-_Biscuits-nGIUtLO_x8g.mp4")
|
||||
// SetFile("/home/drj/MyFiles/Videos/WebShows/YouTube/Willie_Nelson_-_Topic/Too_Sick_To_Pray.Willie_Nelson_-_Topic.8QgBXo41j2E.webm")
|
||||
print()
|
||||
|
||||
quit()
|
||||
}
|
||||
|
||||
func Run(step AppStep) {
|
||||
testPrint()
|
||||
for step < Quit {
|
||||
switch step {
|
||||
case Pick:
|
||||
|
|
@ -91,6 +95,9 @@ func Run(step AppStep) {
|
|||
case Print:
|
||||
print()
|
||||
step = mainMenu()
|
||||
case Convert:
|
||||
convert()
|
||||
step = mainMenu()
|
||||
case Quit:
|
||||
quit()
|
||||
default:
|
||||
|
|
|
|||
21
app/tags.go
Normal file
21
app/tags.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
t "codeberg.org/danjones000/strip-beats/media/tags"
|
||||
)
|
||||
|
||||
var tags t.Tags
|
||||
|
||||
func resetTags() {
|
||||
tags = t.Tags{}
|
||||
}
|
||||
|
||||
func copyTagsFromFile() {
|
||||
if file == nil {
|
||||
panic(errors.New("Missing file"))
|
||||
}
|
||||
|
||||
tags = file.FullTags()
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright © 2023 Dan Jones <danjones@goodevilgenius.org>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
const (
|
||||
AppName string = "strip-beats"
|
||||
Version string = "0.1.0"
|
||||
Version string = "0.1.1"
|
||||
Url string = "https://codeberg.org/danjones000/strip-beats"
|
||||
Email string = "danjones@goodevilgenius.org"
|
||||
UserAgent string = AppName + "/" + Version + " (" + Url + "; " + Email + ")"
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -13,21 +13,25 @@ require (
|
|||
github.com/rivo/tview v0.0.0-20230826224341-9754ab44dc1c
|
||||
github.com/rkoesters/xdg v0.0.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
golang.org/x/term v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.38.20 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/u2takey/go-utils v0.3.1 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
1
go.sum
1
go.sum
|
|
@ -117,6 +117,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
|
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
|||
20
io/message/message.go
Normal file
20
io/message/message.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package message
|
||||
|
||||
import "github.com/rivo/tview"
|
||||
|
||||
func Message(text string) {
|
||||
app := tview.NewApplication()
|
||||
modal := tview.NewModal()
|
||||
if text != "" {
|
||||
modal.SetText(text)
|
||||
}
|
||||
|
||||
modal.AddButtons([]string{"Ok"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
app.Stop()
|
||||
})
|
||||
|
||||
if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
3
main.go
3
main.go
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
Copyright © 2023 Dan Jones <danjones@goodevilgenius.org>
|
||||
*/
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
type Recording struct {
|
||||
Id uuid.UUID
|
||||
AcousticId uuid.UUID
|
||||
Isrcs []string
|
||||
FirstReleaseDate string `json:"first-release-date"`
|
||||
Length int
|
||||
|
|
@ -19,6 +20,45 @@ type Recording struct {
|
|||
Video bool
|
||||
Releases []Release
|
||||
Genres []Genre
|
||||
ArtistCredit []ArtistCredit `json:"artist-credit"`
|
||||
}
|
||||
|
||||
func (r Recording) FirstArtist() Artist {
|
||||
var a Artist
|
||||
for _, ac := range r.ArtistCredit {
|
||||
if ac.Artist.Name != "" {
|
||||
return ac.Artist
|
||||
}
|
||||
}
|
||||
for _, rel := range r.Releases {
|
||||
for _, ac := range rel.ArtistCredit {
|
||||
if ac.Artist.Name != "" {
|
||||
return ac.Artist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if a.Name == "" {
|
||||
a.Name = "Unknown Artist"
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (r Recording) FirstGenre() Genre {
|
||||
var g Genre
|
||||
for _, g = range r.Genres {
|
||||
if g.Name != "" {
|
||||
return g
|
||||
}
|
||||
}
|
||||
for _, rel := range r.Releases {
|
||||
for _, g = range rel.Genres {
|
||||
if g.Name != "" {
|
||||
return g
|
||||
}
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
type Genre struct {
|
||||
|
|
@ -27,21 +67,57 @@ type Genre struct {
|
|||
}
|
||||
|
||||
type Release struct {
|
||||
Id uuid.UUID
|
||||
Country string
|
||||
Date string
|
||||
Media []Media
|
||||
Status string
|
||||
StatusId uuid.UUID `json:"status-id"`
|
||||
ArtistCredit []ArtistCredit `json:"artist-credit"`
|
||||
Title string
|
||||
Genres []Genre
|
||||
Id uuid.UUID
|
||||
Asin string
|
||||
Barcode string
|
||||
Country string
|
||||
Date string
|
||||
Disambiguation string
|
||||
Media []Media
|
||||
Packaging string
|
||||
PackagingId uuid.UUID `json:"packaging-id"`
|
||||
Quality string
|
||||
Status string
|
||||
StatusId uuid.UUID `json:"status-id"`
|
||||
ArtistCredit []ArtistCredit `json:"artist-credit"`
|
||||
Title string
|
||||
Genres []Genre
|
||||
ReleaseGroup ReleaseGroup `json:"release-group"`
|
||||
LabelInfo []LabelInfo `json:"label-info"`
|
||||
// ReleaseEvents []ReleaseEvent `json:"release-events"`
|
||||
}
|
||||
|
||||
type LabelInfo struct {
|
||||
CatalogNumber string `json:"catalog-number"`
|
||||
Label Label
|
||||
}
|
||||
|
||||
type Label struct {
|
||||
Id uuid.UUID
|
||||
Name string
|
||||
SortName string `json:"sort-name"`
|
||||
Disambiguation string
|
||||
TypeId string `json:"type-id"`
|
||||
Type string
|
||||
LabelCode int `json:"label-code"`
|
||||
}
|
||||
|
||||
type ReleaseGroup struct {
|
||||
Id uuid.UUID
|
||||
Title string
|
||||
ArtistCredit []ArtistCredit `json:"artist-credit"`
|
||||
Disambiguation string
|
||||
FirstReleaseDate string `json:"first-release-date"`
|
||||
PrimaryType string `json:"primary-type"`
|
||||
PrimaryTypeId uuid.UUID `json:"primary-type-id"`
|
||||
SecondaryTypes []string `json:"secondary-types"`
|
||||
SecondaryTypeIds []uuid.UUID `json:"secondary-type-ids"`
|
||||
}
|
||||
|
||||
type ArtistCredit struct {
|
||||
Name string
|
||||
Artist Artist
|
||||
Name string
|
||||
Artist Artist
|
||||
JoinPhrase string
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
|
|
@ -56,6 +132,7 @@ type Artist struct {
|
|||
type Media struct {
|
||||
FormatId uuid.UUID `json:"format-id"`
|
||||
Position int
|
||||
Title string
|
||||
TrackOffset int `json:"track-offset"`
|
||||
Format string
|
||||
TrackCount int `json:"track-count"`
|
||||
|
|
@ -70,6 +147,25 @@ type Track struct {
|
|||
Length int
|
||||
}
|
||||
|
||||
func GetReleaseWithMedia(id uuid.UUID) (Release, error) {
|
||||
rel := Release{Id: id}
|
||||
url := fmt.Sprintf("https://musicbrainz.org/ws/2/release/%s", id)
|
||||
resp, err := h.GetWithQuery(url, u.Values{
|
||||
"fmt": []string{"json"},
|
||||
"inc": []string{"artist-credits+discids+labels+release-groups"}})
|
||||
if err != nil {
|
||||
return rel, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return rel, err
|
||||
}
|
||||
err = json.Unmarshal(body, &rel)
|
||||
|
||||
return rel, err
|
||||
}
|
||||
|
||||
func GetRecording(id string) (Recording, error) {
|
||||
u, err := uuid.Parse(id)
|
||||
rec := Recording{Id: u}
|
||||
|
|
|
|||
130
media/ffmpeg.go
130
media/ffmpeg.go
|
|
@ -3,12 +3,142 @@ package media
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
p "path/filepath"
|
||||
"strconv"
|
||||
s "strings"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/config"
|
||||
t "codeberg.org/danjones000/strip-beats/media/tags"
|
||||
"codeberg.org/danjones000/strip-beats/utils"
|
||||
"github.com/google/uuid"
|
||||
ffmpeg "github.com/u2takey/ffmpeg-go"
|
||||
)
|
||||
|
||||
func ConvertAndTag(in Probe, tags t.Tags) (string, error) {
|
||||
st := in.GetFirstAcceptableAudio()
|
||||
if st == nil {
|
||||
st = in.GetFirstAudio()
|
||||
}
|
||||
if st == nil {
|
||||
return "", errors.New("Can't find an audio stream")
|
||||
}
|
||||
|
||||
conf := config.GetConfig()
|
||||
codec := utils.Tern(st.isAcceptableCodec(), "copy", conf.FfEncoder)
|
||||
target := utils.Tern(st.isAcceptableCodec(), st.CodecName, conf.Codec)
|
||||
|
||||
base := p.Base(in.Format.Path)
|
||||
ext := p.Ext(base)
|
||||
base = s.TrimSuffix(base, ext)
|
||||
ext = conf.CodecExt[target]
|
||||
|
||||
out := p.Join(conf.SavePath, base+"."+ext)
|
||||
|
||||
input := ffmpeg.Input(in.Format.Path).Get(strconv.Itoa(st.Index))
|
||||
args := ffmpeg.KwArgs{"c:a": codec}
|
||||
output := input.Output(out, args, getMetadataArgs(ext, tags)).GlobalArgs("-y")
|
||||
|
||||
return out, output.Run()
|
||||
}
|
||||
|
||||
func getMetadataArgs(ext string, tags t.Tags) ffmpeg.KwArgs {
|
||||
var meta []string
|
||||
args := ffmpeg.KwArgs{}
|
||||
switch ext {
|
||||
case "opus":
|
||||
fallthrough
|
||||
case "flac":
|
||||
fallthrough
|
||||
case "ogg":
|
||||
meta = append(meta, mapMetaKeys(getOggMeta(tags))...)
|
||||
case "m4a":
|
||||
fallthrough
|
||||
case "mp4":
|
||||
meta = append(meta, mapMetaKeys(getMp4Meta(tags))...)
|
||||
case "mp3":
|
||||
// @todo meta = append(meta, mapMetaKeys(getMp3Meta(tags))...)
|
||||
}
|
||||
|
||||
meta = append(meta, "comment=Processed by "+config.UserAgent)
|
||||
args["metadata"] = meta
|
||||
return args
|
||||
}
|
||||
|
||||
func mapMetaKeys(meta map[string]string) []string {
|
||||
out := []string{}
|
||||
for k, v := range meta {
|
||||
if v != "" {
|
||||
out = append(out, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func getOggMeta(tags t.Tags) map[string]string {
|
||||
meta := map[string]string{}
|
||||
meta["TITLE"] = tags.Title
|
||||
meta["ARTIST"] = tags.Artist
|
||||
meta["ALBUM_ARTIST"] = tags.AlbumArtist
|
||||
meta["ALBUMARTIST"] = tags.AlbumArtist
|
||||
meta["ALBUM"] = tags.Album
|
||||
meta["DATE"] = tags.Date
|
||||
year, _, _ := s.Cut(tags.Date, "-")
|
||||
meta["YEAR"] = year
|
||||
meta["URL"] = tags.Url
|
||||
meta["PURL"] = tags.Url
|
||||
meta["URI"] = tags.Url
|
||||
meta["ARTISTSORT"] = tags.ArtistSort
|
||||
meta["ALBUMARTISTSORT"] = tags.AlbumArtistSort
|
||||
meta["RELEASECOUNTRY"] = tags.ReleaseCountry
|
||||
meta["LABEL"] = tags.Label
|
||||
meta["GENRE"] = tags.Genre
|
||||
meta["DISC"] = utils.Tern(tags.Disc > 0, strconv.Itoa(tags.Disc), "")
|
||||
meta["DISCTOTAL"] = utils.Tern(tags.DiscCount > 0, strconv.Itoa(tags.DiscCount), "")
|
||||
meta["TRACK"] = utils.Tern(tags.Track > 0, strconv.Itoa(tags.Track), "")
|
||||
meta["TRACKTOTAL"] = utils.Tern(tags.TrackCount > 0, strconv.Itoa(tags.TrackCount), "")
|
||||
if tags.AcoustidId != uuid.Nil {
|
||||
meta["ACOUSTID_ID"] = tags.AcoustidId.String()
|
||||
}
|
||||
if tags.MusicbrainzReleaseGroupId != uuid.Nil {
|
||||
meta["MUSICBRAINZ_RELEASEGROUPID"] = tags.MusicbrainzReleaseGroupId.String()
|
||||
}
|
||||
if tags.MusicbrainzAlbumId != uuid.Nil {
|
||||
meta["MUSICBRAINZ_ALBUMID"] = tags.MusicbrainzAlbumId.String()
|
||||
}
|
||||
if tags.MusicbrainzAlbumArtistId != uuid.Nil {
|
||||
meta["MUSICBRAINZ_ALBUMARTISTID"] = tags.MusicbrainzAlbumArtistId.String()
|
||||
}
|
||||
if tags.MusicbrainzRecordingId != uuid.Nil {
|
||||
meta["MUSICBRAINZ_RECORDINGID"] = tags.MusicbrainzRecordingId.String()
|
||||
}
|
||||
if tags.MusicBrainzLabelId != uuid.Nil {
|
||||
meta["MUSICBRAINZ_LABELID"] = tags.MusicBrainzLabelId.String()
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
func getMp4Meta(tags t.Tags) map[string]string {
|
||||
meta := map[string]string{}
|
||||
meta["title"] = tags.Title
|
||||
meta["artist"] = tags.Artist
|
||||
meta["album_artist"] = tags.AlbumArtist
|
||||
meta["album"] = tags.Album
|
||||
year, _, _ := s.Cut(tags.Date, "-")
|
||||
meta["year"] = year
|
||||
meta["description"] = tags.Url
|
||||
meta["genre"] = tags.Genre
|
||||
meta["network"] = tags.Label
|
||||
if tags.Disc > 0 {
|
||||
meta["disc"] = fmt.Sprintf("%d", tags.Disc) + utils.Tern(tags.DiscCount > 0, fmt.Sprintf("/%d", tags.DiscCount), "")
|
||||
}
|
||||
if tags.Track > 0 {
|
||||
meta["track"] = fmt.Sprintf("%d", tags.Track) + utils.Tern(tags.TrackCount > 0, fmt.Sprintf("/%d", tags.TrackCount), "")
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
func TrimWithFade(in Probe, out string, start, stop, up, down float64) error {
|
||||
// -ss (start) -t (end) -af afade=t=in:st=(start):d=(up),afade=t=out:st=(downstart):d=(down)
|
||||
st := in.GetFirstAcceptableAudio()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"codeberg.org/danjones000/strip-beats/config"
|
||||
"codeberg.org/danjones000/strip-beats/media/brainz"
|
||||
h "codeberg.org/danjones000/strip-beats/utils/http"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type FPrint struct {
|
||||
|
|
@ -45,7 +46,7 @@ type IdResults struct {
|
|||
}
|
||||
|
||||
type IdResult struct {
|
||||
Id string
|
||||
Id uuid.UUID
|
||||
Score float64
|
||||
Recordings []brainz.Recording
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"codeberg.org/danjones000/strip-beats/config"
|
||||
"codeberg.org/danjones000/strip-beats/media/tags"
|
||||
"codeberg.org/danjones000/strip-beats/utils"
|
||||
"dario.cat/mergo"
|
||||
"github.com/akrennmair/slice"
|
||||
|
|
@ -99,7 +100,7 @@ type Stream struct {
|
|||
StartTime float64 `json:"start_time,string"`
|
||||
Duration float64 `json:",string"`
|
||||
DurationTs int `json:"duration_ts"`
|
||||
Tags Tags
|
||||
Tags tags.Tags
|
||||
}
|
||||
|
||||
func (st Stream) isWantedCodec() bool {
|
||||
|
|
@ -124,26 +125,10 @@ type Format struct {
|
|||
Size int `json:",string"`
|
||||
BitRate int `json:"bit_rate,string"`
|
||||
Score int `json:"probe_score"`
|
||||
Tags Tags
|
||||
Tags tags.Tags
|
||||
}
|
||||
|
||||
type Tags struct {
|
||||
MajorBrand string `json:"major_brand"`
|
||||
Title string
|
||||
Artist string
|
||||
AlbumArtist string `json:"album_artist"`
|
||||
Album string
|
||||
Date string
|
||||
Encoder string
|
||||
Comment string
|
||||
Description string
|
||||
Composer string
|
||||
Genre string
|
||||
Disc string
|
||||
Track string
|
||||
}
|
||||
|
||||
func (pr Probe) FullTags() Tags {
|
||||
func (pr Probe) FullTags() tags.Tags {
|
||||
t := pr.Format.Tags
|
||||
s := pr.WantedAudioStream()
|
||||
if s != nil {
|
||||
|
|
|
|||
35
media/tags/tags.go
Normal file
35
media/tags/tags.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package tags
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Tags struct {
|
||||
Title string
|
||||
Artist string
|
||||
AlbumArtist string `json:"album_artist"`
|
||||
Album string
|
||||
Date string
|
||||
Comment string
|
||||
Description string
|
||||
Synopsis string
|
||||
Url string `json:"purl"`
|
||||
AcoustidId uuid.UUID `json:"ACOUSTID_ID"`
|
||||
MusicbrainzReleaseGroupId uuid.UUID `json:"MUSICBRAINZ_RELEASEGROUPID"`
|
||||
MusicbrainzAlbumId uuid.UUID `json:"MUSICBRAINZ_ALBUMID"`
|
||||
MusicbrainzAlbumArtistId uuid.UUID `json:"MUSICBRAINZ_ALBUMARTISTID"`
|
||||
MusicbrainzRecordingId uuid.UUID
|
||||
MusicBrainzLabelId uuid.UUID
|
||||
ArtistSort string
|
||||
AlbumArtistSort string
|
||||
ReleaseCountry string
|
||||
Label string
|
||||
Composer string
|
||||
Genre string
|
||||
Disc int `json:"-"`
|
||||
DiscCount int
|
||||
Track int `json:"-"`
|
||||
TrackCount int
|
||||
TrackStr string `json:"track"`
|
||||
DiscStr string `json:"disc"`
|
||||
}
|
||||
|
|
@ -29,3 +29,17 @@ func HourMinSecToSeconds(time string) (float64, error) {
|
|||
dur, _ := t.ParseDuration(f)
|
||||
return dur.Seconds(), nil
|
||||
}
|
||||
|
||||
func Tern[V any](choice bool, one, two V) V {
|
||||
if choice {
|
||||
return one
|
||||
}
|
||||
return two
|
||||
}
|
||||
|
||||
func TernCall[V any](choice bool, one, two func() V) V {
|
||||
if choice {
|
||||
return one()
|
||||
}
|
||||
return two()
|
||||
}
|
||||
|
|
|
|||
23
utils/utils_tern_test.go
Normal file
23
utils/utils_tern_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTern(t *testing.T) {
|
||||
assert.Equal(t, Tern(true, 5, 2), 5)
|
||||
assert.Equal(t, Tern(false, 5, 2), 2)
|
||||
}
|
||||
|
||||
func TestTernCall(t *testing.T) {
|
||||
five := func() int {
|
||||
return 5
|
||||
}
|
||||
two := func() int {
|
||||
return 2
|
||||
}
|
||||
assert.Equal(t, TernCall(true, five, two), 5)
|
||||
assert.Equal(t, TernCall(false, five, two), 2)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue