feat(16-02): add go-flac dependencies and implement FLAC tag writer
- Add go-flac/go-flac/v2, flacvorbis/v2, flacpicture/v2 dependencies - Create tagwriter.go with TagChanges type, field constants, format detection, MIME detection - Create flac.go with writeFlacTags using Vorbis Comments + PICTURE blocks + AtomicWrite - Implement replaceVorbisComment helper for case-insensitive field replacement - Handle cover art add/replace/clear via PICTURE metadata blocks
This commit is contained in:
@@ -26,3 +26,9 @@ WHERE id = ?;
|
|||||||
|
|
||||||
-- name: DeleteAllArtistCredits :exec
|
-- name: DeleteAllArtistCredits :exec
|
||||||
DELETE FROM artist_credit;
|
DELETE FROM artist_credit;
|
||||||
|
|
||||||
|
-- name: CountArtistCreditReferences :one
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?1) +
|
||||||
|
(SELECT COUNT(*) FROM release_groups WHERE album_artist_credit_id = ?1)
|
||||||
|
AS total;
|
||||||
|
|||||||
@@ -103,6 +103,12 @@ LEFT JOIN file_types ft ON af.file_type_id = ft.id
|
|||||||
WHERE g.name = ? AND af.library_id = ?
|
WHERE g.name = ? AND af.library_id = ?
|
||||||
ORDER BY r.name;
|
ORDER BY r.name;
|
||||||
|
|
||||||
|
-- name: CountGenreReferences :one
|
||||||
|
SELECT COUNT(*) FROM recording_genres WHERE genre_id = ?;
|
||||||
|
|
||||||
|
-- name: DeleteGenre :exec
|
||||||
|
DELETE FROM genres WHERE id = ?;
|
||||||
|
|
||||||
-- name: GetAllGenresWithCounts :many
|
-- name: GetAllGenresWithCounts :many
|
||||||
SELECT g.name, COUNT(rg.recording_id) AS track_count
|
SELECT g.name, COUNT(rg.recording_id) AS track_count
|
||||||
FROM genres g
|
FROM genres g
|
||||||
|
|||||||
@@ -34,3 +34,6 @@ DELETE FROM recordings;
|
|||||||
-- name: GetAllRecordings :many
|
-- name: GetAllRecordings :many
|
||||||
SELECT * FROM recordings
|
SELECT * FROM recordings
|
||||||
ORDER BY name;
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- name: CountRecordingsByArtistCredit :one
|
||||||
|
SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?;
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ LEFT JOIN (
|
|||||||
WHERE aca.artist_id = ?
|
WHERE aca.artist_id = ?
|
||||||
ORDER BY rg.name;
|
ORDER BY rg.name;
|
||||||
|
|
||||||
|
-- name: CountReleaseGroupRecordings :one
|
||||||
|
SELECT COUNT(*) FROM release_group_recordings WHERE release_group_id = ?;
|
||||||
|
|
||||||
-- name: GetAlbumsByArtistByLibrary :many
|
-- name: GetAlbumsByArtistByLibrary :many
|
||||||
SELECT
|
SELECT
|
||||||
rg.id,
|
rg.id,
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const countArtistCreditReferences = `-- name: CountArtistCreditReferences :one
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?1) +
|
||||||
|
(SELECT COUNT(*) FROM release_groups WHERE album_artist_credit_id = ?1)
|
||||||
|
AS total
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountArtistCreditReferences(ctx context.Context, artistCreditID int64) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countArtistCreditReferences, artistCreditID)
|
||||||
|
var total int64
|
||||||
|
err := row.Scan(&total)
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
|
||||||
const createArtistCredit = `-- name: CreateArtistCredit :one
|
const createArtistCredit = `-- name: CreateArtistCredit :one
|
||||||
INSERT INTO artist_credit (text) VALUES (?)
|
INSERT INTO artist_credit (text) VALUES (?)
|
||||||
RETURNING id, text
|
RETURNING id, text
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const countGenreReferences = `-- name: CountGenreReferences :one
|
||||||
|
SELECT COUNT(*) FROM recording_genres WHERE genre_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountGenreReferences(ctx context.Context, genreID int64) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countGenreReferences, genreID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const createRecordingGenre = `-- name: CreateRecordingGenre :exec
|
const createRecordingGenre = `-- name: CreateRecordingGenre :exec
|
||||||
INSERT OR IGNORE INTO recording_genres (recording_id, genre_id)
|
INSERT OR IGNORE INTO recording_genres (recording_id, genre_id)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
@@ -43,6 +54,15 @@ func (q *Queries) DeleteAllRecordingGenres(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteGenre = `-- name: DeleteGenre :exec
|
||||||
|
DELETE FROM genres WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteGenre(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteGenre, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteRecordingGenres = `-- name: DeleteRecordingGenres :exec
|
const deleteRecordingGenres = `-- name: DeleteRecordingGenres :exec
|
||||||
DELETE FROM recording_genres
|
DELETE FROM recording_genres
|
||||||
WHERE recording_id = ?
|
WHERE recording_id = ?
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const countRecordingsByArtistCredit = `-- name: CountRecordingsByArtistCredit :one
|
||||||
|
SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountRecordingsByArtistCredit(ctx context.Context, artistCreditID int64) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countRecordingsByArtistCredit, artistCreditID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const createRecording = `-- name: CreateRecording :one
|
const createRecording = `-- name: CreateRecording :one
|
||||||
INSERT INTO recordings (name, artist_credit_id) VALUES (?, ?)
|
INSERT INTO recordings (name, artist_credit_id) VALUES (?, ?)
|
||||||
RETURNING id, name, artist_credit_id, track_number, disc_number, year, genre, composer, lyrics, comment
|
RETURNING id, name, artist_credit_id, track_number, disc_number, year, genre, composer, lyrics, comment
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const countReleaseGroupRecordings = `-- name: CountReleaseGroupRecordings :one
|
||||||
|
SELECT COUNT(*) FROM release_group_recordings WHERE release_group_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountReleaseGroupRecordings(ctx context.Context, releaseGroupID int64) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countReleaseGroupRecordings, releaseGroupID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const createReleaseGroup = `-- name: CreateReleaseGroup :one
|
const createReleaseGroup = `-- name: CreateReleaseGroup :one
|
||||||
INSERT INTO release_groups (name) VALUES (?)
|
INSERT INTO release_groups (name) VALUES (?)
|
||||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package tagwriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-flac/flacpicture/v2"
|
||||||
|
"github.com/go-flac/flacvorbis/v2"
|
||||||
|
flac "github.com/go-flac/go-flac/v2"
|
||||||
|
|
||||||
|
"yellowjacket/backend/fileutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writeFlacTags writes metadata tags to a FLAC file using Vorbis Comments
|
||||||
|
// and PICTURE metadata blocks, integrated with AtomicWrite for crash safety.
|
||||||
|
func writeFlacTags(logger *slog.Logger, filePath string, changes TagChanges) error {
|
||||||
|
// Check file size and warn for very large files.
|
||||||
|
if info, err := os.Stat(filePath); err == nil {
|
||||||
|
const largeSizeThreshold = 500 * 1024 * 1024 // 500 MB
|
||||||
|
if info.Size() > largeSizeThreshold {
|
||||||
|
logger.Warn("large FLAC file may use significant memory",
|
||||||
|
slog.String("path", filePath),
|
||||||
|
slog.Int64("size", info.Size()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := flac.ParseFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse flac: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing Vorbis Comment block.
|
||||||
|
var cmt *flacvorbis.MetaDataBlockVorbisComment
|
||||||
|
|
||||||
|
cmtIdx := -1
|
||||||
|
|
||||||
|
for idx, meta := range f.Meta {
|
||||||
|
if meta.Type == flac.VorbisComment {
|
||||||
|
cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse vorbis comments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmtIdx = idx
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmt == nil {
|
||||||
|
cmt = flacvorbis.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text changes from the diff map.
|
||||||
|
if err := applyFlacTextChanges(cmt, changes); err != nil {
|
||||||
|
return fmt.Errorf("apply flac text changes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal Vorbis Comment block back and update f.Meta.
|
||||||
|
cmtMeta := cmt.Marshal()
|
||||||
|
if cmtIdx >= 0 {
|
||||||
|
f.Meta[cmtIdx] = &cmtMeta
|
||||||
|
} else {
|
||||||
|
f.Meta = append(f.Meta, &cmtMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cover art — PICTURE metadata block.
|
||||||
|
if err := applyFlacCoverArt(f, changes); err != nil {
|
||||||
|
return fmt.Errorf("apply flac cover art: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write atomically via AtomicWrite.
|
||||||
|
return fileutil.AtomicWrite(logger, filePath, func(tmp *os.File) error {
|
||||||
|
_, writeErr := f.WriteTo(tmp)
|
||||||
|
if writeErr != nil {
|
||||||
|
return fmt.Errorf("write flac: %w", writeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyFlacTextChanges applies text field changes to a Vorbis Comment block.
|
||||||
|
func applyFlacTextChanges(cmt *flacvorbis.MetaDataBlockVorbisComment, changes TagChanges) error {
|
||||||
|
type fieldMapping struct {
|
||||||
|
key string
|
||||||
|
vorbisID string
|
||||||
|
isInt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings := []fieldMapping{
|
||||||
|
{FieldTitle, flacvorbis.FIELD_TITLE, false},
|
||||||
|
{FieldArtist, flacvorbis.FIELD_ARTIST, false},
|
||||||
|
{FieldAlbum, flacvorbis.FIELD_ALBUM, false},
|
||||||
|
{FieldAlbumArtist, "ALBUMARTIST", false},
|
||||||
|
{FieldGenre, flacvorbis.FIELD_GENRE, false},
|
||||||
|
{FieldYear, flacvorbis.FIELD_DATE, true},
|
||||||
|
{FieldTrackNumber, flacvorbis.FIELD_TRACKNUMBER, true},
|
||||||
|
{FieldDiscNumber, "DISCNUMBER", true},
|
||||||
|
{FieldComposer, "COMPOSER", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range mappings {
|
||||||
|
v, ok := changes[m.key]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var val string
|
||||||
|
if m.isInt {
|
||||||
|
val = strconv.Itoa(v.(int))
|
||||||
|
} else {
|
||||||
|
val = v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceVorbisComment(cmt, m.vorbisID, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceVorbisComment removes all existing entries for a field and adds a
|
||||||
|
// new value. Vorbis Comment field names are case-insensitive per spec.
|
||||||
|
func replaceVorbisComment(cmt *flacvorbis.MetaDataBlockVorbisComment, field string, value string) {
|
||||||
|
// Remove all existing entries for this field (case-insensitive).
|
||||||
|
prefix := strings.ToUpper(field) + "="
|
||||||
|
filtered := make([]string, 0, len(cmt.Comments))
|
||||||
|
|
||||||
|
for _, c := range cmt.Comments {
|
||||||
|
if !strings.HasPrefix(strings.ToUpper(c), prefix) {
|
||||||
|
filtered = append(filtered, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmt.Comments = filtered
|
||||||
|
|
||||||
|
// Add the new value. Using the uppercase field name (Vorbis convention).
|
||||||
|
_ = cmt.Add(strings.ToUpper(field), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyFlacCoverArt handles adding, replacing, or clearing PICTURE metadata
|
||||||
|
// blocks in a FLAC file.
|
||||||
|
func applyFlacCoverArt(f *flac.File, changes TagChanges) error {
|
||||||
|
v, ok := changes[FieldCoverArt]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all existing PICTURE blocks.
|
||||||
|
newMeta := make([]*flac.MetaDataBlock, 0, len(f.Meta))
|
||||||
|
|
||||||
|
for _, meta := range f.Meta {
|
||||||
|
if meta.Type != flac.Picture {
|
||||||
|
newMeta = append(newMeta, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Meta = newMeta
|
||||||
|
|
||||||
|
// If value is nil or empty, we've cleared the art — done.
|
||||||
|
data, isBytes := v.([]byte)
|
||||||
|
if !isBytes || len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new PICTURE block with the provided image data.
|
||||||
|
pic, err := flacpicture.NewFromImageData(
|
||||||
|
flacpicture.PictureTypeFrontCover,
|
||||||
|
"Front cover",
|
||||||
|
data,
|
||||||
|
detectMIME(data),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create flac picture: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
picMeta := pic.Marshal()
|
||||||
|
f.Meta = append(f.Meta, &picMeta)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// Package tagwriter writes metadata tags to audio files.
|
||||||
|
package tagwriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagChanges is a diff map of field name → new value. Only changed
|
||||||
|
// fields are present. Callers specify changed fields; unchanged
|
||||||
|
// fields are left as-is in the file.
|
||||||
|
type TagChanges map[string]any
|
||||||
|
|
||||||
|
// Field name constants for the diff map.
|
||||||
|
const (
|
||||||
|
FieldTitle = "title"
|
||||||
|
FieldArtist = "artist"
|
||||||
|
FieldAlbum = "album"
|
||||||
|
FieldAlbumArtist = "album_artist"
|
||||||
|
FieldGenre = "genre"
|
||||||
|
FieldYear = "year"
|
||||||
|
FieldTrackNumber = "track_number"
|
||||||
|
FieldDiscNumber = "disc_number"
|
||||||
|
FieldComposer = "composer"
|
||||||
|
FieldCoverArt = "cover_art" // []byte for set, nil for clear
|
||||||
|
)
|
||||||
|
|
||||||
|
// AudioFormat represents a supported audio file format.
|
||||||
|
type AudioFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FormatMP3 is the MP3 audio format.
|
||||||
|
FormatMP3 AudioFormat = "mp3"
|
||||||
|
// FormatFLAC is the FLAC audio format.
|
||||||
|
FormatFLAC AudioFormat = "flac"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errUnsupportedFormat is returned when the audio format is not supported.
|
||||||
|
var errUnsupportedFormat = errors.New("tagwriter: unsupported audio format")
|
||||||
|
|
||||||
|
// DetectFormat determines the audio format of a file from its extension.
|
||||||
|
func DetectFormat(filePath string) (AudioFormat, error) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case ".mp3":
|
||||||
|
return FormatMP3, nil
|
||||||
|
case ".flac":
|
||||||
|
return FormatFLAC, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("%w: %s", errUnsupportedFormat, ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectMIME returns the MIME type of image data by checking magic bytes.
|
||||||
|
func detectMIME(data []byte) string {
|
||||||
|
if len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
|
||||||
|
return "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) >= 4 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
|
||||||
|
return "image/png"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// id3v2OriginalTagSize reads an MP3 file's ID3v2 header and returns the
|
||||||
|
// total number of bytes occupied by the tag (10-byte header + body).
|
||||||
|
// If the file does not start with an ID3v2 header it returns 0.
|
||||||
|
func id3v2OriginalTagSize(path string) (int64, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("open for tag size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
// ID3v2 header: "ID3" (3 B) + version (2 B) + flags (1 B) + size (4 B synchsafe).
|
||||||
|
var hdr [10]byte
|
||||||
|
if _, err := f.Read(hdr[:]); err != nil {
|
||||||
|
return 0, fmt.Errorf("read id3v2 header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(hdr[:3]) != "ID3" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
size := decodeSynchSafe(hdr[6:10])
|
||||||
|
|
||||||
|
const headerBytes = 10
|
||||||
|
|
||||||
|
return headerBytes + int64(size), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeSynchSafe decodes a 4-byte synchsafe integer (7 bits per byte).
|
||||||
|
func decodeSynchSafe(b []byte) uint32 {
|
||||||
|
_ = b[3] // bounds check hint
|
||||||
|
|
||||||
|
return uint32(b[0])<<21 |
|
||||||
|
uint32(b[1])<<14 |
|
||||||
|
uint32(b[2])<<7 |
|
||||||
|
uint32(b[3])
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ require (
|
|||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||||
|
github.com/bogem/id3v2/v2 v2.1.4 // indirect
|
||||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||||
github.com/bombsimon/wsl/v5 v5.6.0 // indirect
|
github.com/bombsimon/wsl/v5 v5.6.0 // indirect
|
||||||
github.com/breml/bidichk v0.3.3 // indirect
|
github.com/breml/bidichk v0.3.3 // indirect
|
||||||
@@ -112,6 +113,9 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/ghostiam/protogetter v0.3.20 // indirect
|
github.com/ghostiam/protogetter v0.3.20 // indirect
|
||||||
github.com/go-critic/go-critic v0.14.3 // indirect
|
github.com/go-critic/go-critic v0.14.3 // indirect
|
||||||
|
github.com/go-flac/flacpicture/v2 v2.0.2 // indirect
|
||||||
|
github.com/go-flac/flacvorbis/v2 v2.0.2 // indirect
|
||||||
|
github.com/go-flac/go-flac/v2 v2.0.4 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.13.2 // indirect
|
github.com/go-git/go-git/v5 v5.13.2 // indirect
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5
|
|||||||
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
|
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
|
||||||
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||||
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||||
|
github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI=
|
||||||
|
github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY=
|
||||||
github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
|
github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
|
||||||
github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
|
github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
|
||||||
github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=
|
github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=
|
||||||
@@ -288,6 +290,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
||||||
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||||
|
github.com/go-flac/flacpicture/v2 v2.0.2 h1:HCaJIVZpxnpdWs6G3ECEVRelzqS5xOi1Ba1AGmtXbzE=
|
||||||
|
github.com/go-flac/flacpicture/v2 v2.0.2/go.mod h1:DMZBPWPAmdLqNhqFSy5ZBs9wyBzOekXutGfP7/TFCuo=
|
||||||
|
github.com/go-flac/flacvorbis/v2 v2.0.2 h1:xCL3OhxrxWkHrbWUBvGNe+6FQ03yLmBbz0v5z4V2PoQ=
|
||||||
|
github.com/go-flac/flacvorbis/v2 v2.0.2/go.mod h1:SwTB5gs13VaM/N7rstwPoUsPibiMKklgwybYP9dYo2g=
|
||||||
|
github.com/go-flac/go-flac/v2 v2.0.4 h1:atf/kFa8U9idtkA//NO22XGr+MzQLeXZecnmP9sYBf0=
|
||||||
|
github.com/go-flac/go-flac/v2 v2.0.4/go.mod h1:sYOlTKxutMW0RDYF+KlD6Zn+VOCZlIFQG/r/usPveCs=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||||
@@ -1207,6 +1215,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
|||||||
Reference in New Issue
Block a user