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"
|
"os"
|
||||||
|
|
||||||
"codeberg.org/danjones000/strip-beats/files"
|
"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/media"
|
||||||
"codeberg.org/danjones000/strip-beats/utils"
|
"codeberg.org/danjones000/strip-beats/utils"
|
||||||
"github.com/rkoesters/xdg/trash"
|
"github.com/rkoesters/xdg/trash"
|
||||||
|
|
@ -20,8 +20,10 @@ func PickNewFile() media.Probe {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFile(path string) media.Probe {
|
func SetFile(path string) media.Probe {
|
||||||
|
resetTags()
|
||||||
f := media.ProbeFile(path)
|
f := media.ProbeFile(path)
|
||||||
file = &f
|
file = &f
|
||||||
|
copyTagsFromFile()
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +72,7 @@ func PickAgain() {
|
||||||
}
|
}
|
||||||
file = nil
|
file = nil
|
||||||
tmpfile = nil
|
tmpfile = nil
|
||||||
|
resetTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Finish() {
|
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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
p "path/filepath"
|
||||||
|
s "strings"
|
||||||
|
|
||||||
"codeberg.org/danjones000/strip-beats/media"
|
"codeberg.org/danjones000/strip-beats/media"
|
||||||
"codeberg.org/danjones000/strip-beats/utils"
|
"codeberg.org/danjones000/strip-beats/utils"
|
||||||
|
|
@ -31,7 +33,10 @@ func validateNumber(input string, lastChar rune) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fadeFile() error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
app/menu.go
13
app/menu.go
|
|
@ -3,14 +3,14 @@ package app
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeberg.org/danjones000/strip-beats/input/list"
|
"codeberg.org/danjones000/strip-beats/io/list"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (st AppStep) Title() string {
|
func (st AppStep) Title() string {
|
||||||
mustpick := "You need to pick a file"
|
mustpick := "You need to pick a file"
|
||||||
switch st {
|
switch st {
|
||||||
case Pick:
|
case Pick:
|
||||||
return "Pick a new show"
|
return "Pick a new file"
|
||||||
case Watch:
|
case Watch:
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return mustpick
|
return mustpick
|
||||||
|
|
@ -26,6 +26,11 @@ func (st AppStep) Title() string {
|
||||||
return mustpick
|
return mustpick
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Should we try to identify %s", file.ShortPath())
|
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:
|
case Restart:
|
||||||
return "Forget current selection"
|
return "Forget current selection"
|
||||||
case Quit:
|
case Quit:
|
||||||
|
|
@ -54,6 +59,8 @@ func (st AppStep) Rune() rune {
|
||||||
return 'r'
|
return 'r'
|
||||||
case Print:
|
case Print:
|
||||||
return 'a'
|
return 'a'
|
||||||
|
case Convert:
|
||||||
|
return 'c'
|
||||||
case Quit:
|
case Quit:
|
||||||
return 'q'
|
return 'q'
|
||||||
default:
|
default:
|
||||||
|
|
@ -70,7 +77,7 @@ func mainMenu() AppStep {
|
||||||
if file == nil {
|
if file == nil {
|
||||||
steps = []list.Option{Pick, Quit}
|
steps = []list.Option{Pick, Quit}
|
||||||
} else {
|
} 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)
|
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 (
|
import (
|
||||||
"fmt"
|
"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"
|
||||||
"codeberg.org/danjones000/strip-beats/media/brainz"
|
"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() {
|
func print() {
|
||||||
if file == nil {
|
if file == nil {
|
||||||
PickFileWithConf()
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -19,17 +52,136 @@ func print() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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 _, res := range ids.Results {
|
||||||
for _, rec := range res.Recordings {
|
for _, rec = range res.Recordings {
|
||||||
err = brainz.FillRecording(&rec)
|
err = brainz.FillRecording(&rec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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"
|
"fmt"
|
||||||
"os"
|
"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"
|
||||||
"codeberg.org/danjones000/strip-beats/media/brainz"
|
"codeberg.org/danjones000/strip-beats/media/brainz"
|
||||||
)
|
)
|
||||||
|
|
@ -16,6 +16,7 @@ const (
|
||||||
Watch
|
Watch
|
||||||
Fade
|
Fade
|
||||||
Print
|
Print
|
||||||
|
Convert
|
||||||
Restart
|
Restart
|
||||||
Quit
|
Quit
|
||||||
)
|
)
|
||||||
|
|
@ -51,14 +52,17 @@ func testMb() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPrint() {
|
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()
|
print()
|
||||||
|
|
||||||
quit()
|
quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(step AppStep) {
|
func Run(step AppStep) {
|
||||||
testPrint()
|
|
||||||
for step < Quit {
|
for step < Quit {
|
||||||
switch step {
|
switch step {
|
||||||
case Pick:
|
case Pick:
|
||||||
|
|
@ -91,6 +95,9 @@ func Run(step AppStep) {
|
||||||
case Print:
|
case Print:
|
||||||
print()
|
print()
|
||||||
step = mainMenu()
|
step = mainMenu()
|
||||||
|
case Convert:
|
||||||
|
convert()
|
||||||
|
step = mainMenu()
|
||||||
case Quit:
|
case Quit:
|
||||||
quit()
|
quit()
|
||||||
default:
|
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
|
package cmd
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AppName string = "strip-beats"
|
AppName string = "strip-beats"
|
||||||
Version string = "0.1.0"
|
Version string = "0.1.1"
|
||||||
Url string = "https://codeberg.org/danjones000/strip-beats"
|
Url string = "https://codeberg.org/danjones000/strip-beats"
|
||||||
Email string = "danjones@goodevilgenius.org"
|
Email string = "danjones@goodevilgenius.org"
|
||||||
UserAgent string = AppName + "/" + Version + " (" + Url + "; " + Email + ")"
|
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/rivo/tview v0.0.0-20230826224341-9754ab44dc1c
|
||||||
github.com/rkoesters/xdg v0.0.1
|
github.com/rkoesters/xdg v0.0.1
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0
|
github.com/u2takey/ffmpeg-go v0.5.0
|
||||||
golang.org/x/term v0.11.0
|
golang.org/x/term v0.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.38.20 // indirect
|
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/encoding v1.0.0 // indirect
|
||||||
github.com/gdamore/tcell/v2 v2.6.0 // indirect
|
github.com/gdamore/tcell/v2 v2.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // 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/rivo/uniseg v0.4.3 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/u2takey/go-utils v0.3.1 // indirect
|
github.com/u2takey/go-utils v0.3.1 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
golang.org/x/text v0.7.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.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/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=
|
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/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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.7/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
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
type Recording struct {
|
type Recording struct {
|
||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
|
AcousticId uuid.UUID
|
||||||
Isrcs []string
|
Isrcs []string
|
||||||
FirstReleaseDate string `json:"first-release-date"`
|
FirstReleaseDate string `json:"first-release-date"`
|
||||||
Length int
|
Length int
|
||||||
|
|
@ -19,6 +20,45 @@ type Recording struct {
|
||||||
Video bool
|
Video bool
|
||||||
Releases []Release
|
Releases []Release
|
||||||
Genres []Genre
|
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 {
|
type Genre struct {
|
||||||
|
|
@ -28,20 +68,56 @@ type Genre struct {
|
||||||
|
|
||||||
type Release struct {
|
type Release struct {
|
||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
|
Asin string
|
||||||
|
Barcode string
|
||||||
Country string
|
Country string
|
||||||
Date string
|
Date string
|
||||||
|
Disambiguation string
|
||||||
Media []Media
|
Media []Media
|
||||||
|
Packaging string
|
||||||
|
PackagingId uuid.UUID `json:"packaging-id"`
|
||||||
|
Quality string
|
||||||
Status string
|
Status string
|
||||||
StatusId uuid.UUID `json:"status-id"`
|
StatusId uuid.UUID `json:"status-id"`
|
||||||
ArtistCredit []ArtistCredit `json:"artist-credit"`
|
ArtistCredit []ArtistCredit `json:"artist-credit"`
|
||||||
Title string
|
Title string
|
||||||
Genres []Genre
|
Genres []Genre
|
||||||
|
ReleaseGroup ReleaseGroup `json:"release-group"`
|
||||||
|
LabelInfo []LabelInfo `json:"label-info"`
|
||||||
// ReleaseEvents []ReleaseEvent `json:"release-events"`
|
// 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 {
|
type ArtistCredit struct {
|
||||||
Name string
|
Name string
|
||||||
Artist Artist
|
Artist Artist
|
||||||
|
JoinPhrase string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Artist struct {
|
type Artist struct {
|
||||||
|
|
@ -56,6 +132,7 @@ type Artist struct {
|
||||||
type Media struct {
|
type Media struct {
|
||||||
FormatId uuid.UUID `json:"format-id"`
|
FormatId uuid.UUID `json:"format-id"`
|
||||||
Position int
|
Position int
|
||||||
|
Title string
|
||||||
TrackOffset int `json:"track-offset"`
|
TrackOffset int `json:"track-offset"`
|
||||||
Format string
|
Format string
|
||||||
TrackCount int `json:"track-count"`
|
TrackCount int `json:"track-count"`
|
||||||
|
|
@ -70,6 +147,25 @@ type Track struct {
|
||||||
Length int
|
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) {
|
func GetRecording(id string) (Recording, error) {
|
||||||
u, err := uuid.Parse(id)
|
u, err := uuid.Parse(id)
|
||||||
rec := Recording{Id: u}
|
rec := Recording{Id: u}
|
||||||
|
|
|
||||||
130
media/ffmpeg.go
130
media/ffmpeg.go
|
|
@ -3,12 +3,142 @@ package media
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
p "path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
s "strings"
|
||||||
|
|
||||||
"codeberg.org/danjones000/strip-beats/config"
|
"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"
|
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 {
|
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)
|
// -ss (start) -t (end) -af afade=t=in:st=(start):d=(up),afade=t=out:st=(downstart):d=(down)
|
||||||
st := in.GetFirstAcceptableAudio()
|
st := in.GetFirstAcceptableAudio()
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"codeberg.org/danjones000/strip-beats/config"
|
"codeberg.org/danjones000/strip-beats/config"
|
||||||
"codeberg.org/danjones000/strip-beats/media/brainz"
|
"codeberg.org/danjones000/strip-beats/media/brainz"
|
||||||
h "codeberg.org/danjones000/strip-beats/utils/http"
|
h "codeberg.org/danjones000/strip-beats/utils/http"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FPrint struct {
|
type FPrint struct {
|
||||||
|
|
@ -45,7 +46,7 @@ type IdResults struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdResult struct {
|
type IdResult struct {
|
||||||
Id string
|
Id uuid.UUID
|
||||||
Score float64
|
Score float64
|
||||||
Recordings []brainz.Recording
|
Recordings []brainz.Recording
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"codeberg.org/danjones000/strip-beats/config"
|
"codeberg.org/danjones000/strip-beats/config"
|
||||||
|
"codeberg.org/danjones000/strip-beats/media/tags"
|
||||||
"codeberg.org/danjones000/strip-beats/utils"
|
"codeberg.org/danjones000/strip-beats/utils"
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/akrennmair/slice"
|
"github.com/akrennmair/slice"
|
||||||
|
|
@ -99,7 +100,7 @@ type Stream struct {
|
||||||
StartTime float64 `json:"start_time,string"`
|
StartTime float64 `json:"start_time,string"`
|
||||||
Duration float64 `json:",string"`
|
Duration float64 `json:",string"`
|
||||||
DurationTs int `json:"duration_ts"`
|
DurationTs int `json:"duration_ts"`
|
||||||
Tags Tags
|
Tags tags.Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st Stream) isWantedCodec() bool {
|
func (st Stream) isWantedCodec() bool {
|
||||||
|
|
@ -124,26 +125,10 @@ type Format struct {
|
||||||
Size int `json:",string"`
|
Size int `json:",string"`
|
||||||
BitRate int `json:"bit_rate,string"`
|
BitRate int `json:"bit_rate,string"`
|
||||||
Score int `json:"probe_score"`
|
Score int `json:"probe_score"`
|
||||||
Tags Tags
|
Tags tags.Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tags struct {
|
func (pr Probe) FullTags() tags.Tags {
|
||||||
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 {
|
|
||||||
t := pr.Format.Tags
|
t := pr.Format.Tags
|
||||||
s := pr.WantedAudioStream()
|
s := pr.WantedAudioStream()
|
||||||
if s != nil {
|
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)
|
dur, _ := t.ParseDuration(f)
|
||||||
return dur.Seconds(), nil
|
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