mirror of
https://github.com/AynaLivePlayer/miaosic.git
synced 2025-12-06 13:02:48 +08:00
add download cmd
This commit is contained in:
186
cmd/miaosic/cmds/download.go
Normal file
186
cmd/miaosic/cmds/download.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
"github.com/AynaLivePlayer/miaosic/cmd/miaosic/cmds/tagwriter"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
writeMetadata bool
|
||||
downloadQuality string
|
||||
)
|
||||
|
||||
func init() {
|
||||
CmdDownload.Flags().BoolVar(&writeMetadata, "metadata", true, "Write metadata (tags, cover, lyrics) to the file")
|
||||
CmdDownload.Flags().StringVar(&downloadQuality, "quality", "", "Quality preference (e.g., 128k, 320k, flac)")
|
||||
}
|
||||
|
||||
var CmdDownload = &cobra.Command{
|
||||
Use: "download <provider> <uri>",
|
||||
Short: "Download media, with metadata and cover art",
|
||||
Long: `Downloads a media file from a provider.
|
||||
It fetches media information, URL, lyrics, and cover art.
|
||||
By default, it writes all available metadata to the downloaded file.
|
||||
Supported formats for metadata include MP3 and FLAC.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Steps 1-3: Get provider, media info, and URL (this part is unchanged)
|
||||
providerName := args[0]
|
||||
uri := args[1]
|
||||
provider, ok := miaosic.GetProvider(providerName)
|
||||
if !ok {
|
||||
fmt.Printf("Provider not found: %s\n", providerName)
|
||||
return
|
||||
}
|
||||
meta, ok := provider.MatchMedia(uri)
|
||||
if !ok {
|
||||
fmt.Printf("URI not matched by provider: %s\n", uri)
|
||||
return
|
||||
}
|
||||
fmt.Println("Fetching media info...")
|
||||
info, err := provider.GetMediaInfo(meta)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting media info: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Found: %s - %s\n", info.Artist, info.Title)
|
||||
|
||||
fmt.Println("Fetching media URL...")
|
||||
urls, err := provider.GetMediaUrl(meta, miaosic.Quality(downloadQuality))
|
||||
if err != nil || len(urls) == 0 {
|
||||
fmt.Printf("Error getting media URL or no URL found: %v\n", err)
|
||||
return
|
||||
}
|
||||
mediaURL := urls[0]
|
||||
fmt.Printf("Selected quality: %s\n", mediaURL.Quality)
|
||||
|
||||
// Step 4: Download media file with progress bar (this part is unchanged)
|
||||
resp, err := http.Get(mediaURL.Url) // Simplified GET for clarity
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting download: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Error downloading file: server responded with status %d\n", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
totalSize, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
||||
bar := progressbar.NewOptions64(totalSize,
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Downloading %s...", info.Title)),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetWidth(40),
|
||||
progressbar.OptionOnCompletion(func() { fmt.Fprint(os.Stderr, "\n") }),
|
||||
)
|
||||
|
||||
// Create a buffer and copy the download into it, with progress
|
||||
mediaData := &bytes.Buffer{}
|
||||
pRead := progressbar.NewReader(resp.Body, bar)
|
||||
_, err = io.Copy(mediaData, &pRead)
|
||||
if err != nil {
|
||||
fmt.Printf("Error during download: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// *** NEW: Detect content type from the first 512 bytes ***
|
||||
downloadedBytes := mediaData.Bytes()
|
||||
detectedContentType := mimetype.Detect(downloadedBytes[:min(512, len(downloadedBytes))]).String()
|
||||
|
||||
ext, err := extensionFromContentType(detectedContentType)
|
||||
if err != nil {
|
||||
// Fallback strategy if detection is inconclusive
|
||||
fmt.Printf("Warning: Could not determine file type from content (%s). Falling back to URL extension.\n", detectedContentType)
|
||||
parsedURL, urlErr := url.Parse(mediaURL.Url)
|
||||
if urlErr != nil {
|
||||
// If URL is malformed, we can't get an extension.
|
||||
ext = ""
|
||||
} else {
|
||||
// Get extension from the path, which has no query string.
|
||||
ext = filepath.Ext(parsedURL.Path)
|
||||
}
|
||||
if ext == "" {
|
||||
fmt.Println("Warning: Could not determine file type from URL. Defaulting to .mp3.")
|
||||
ext = ".mp3" // Final fallback
|
||||
}
|
||||
}
|
||||
fmt.Printf("Detected file type: %s (%s)\n", detectedContentType, ext)
|
||||
|
||||
// Step 5: Save the file from the buffer
|
||||
filename := sanitizeFilename(fmt.Sprintf("%s - %s%s", info.Artist, info.Title, ext))
|
||||
err = os.WriteFile(filename, downloadedBytes, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving file to disk: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If metadata writing is disabled, we are done.
|
||||
if !writeMetadata {
|
||||
fmt.Printf("Download complete! Saved to %s\n", filename)
|
||||
return
|
||||
}
|
||||
|
||||
// Step 6: Write Metadata (same as before)
|
||||
fmt.Println("Writing metadata...")
|
||||
lyric, _ := provider.GetMediaLyric(meta)
|
||||
var coverData []byte
|
||||
if info.Cover.Url != "" {
|
||||
fmt.Println("Downloading cover art...")
|
||||
coverResp, err := http.Get(info.Cover.Url)
|
||||
if err == nil && coverResp.StatusCode == http.StatusOK {
|
||||
defer coverResp.Body.Close()
|
||||
coverData, _ = io.ReadAll(coverResp.Body)
|
||||
info.Cover.Data = coverData
|
||||
fmt.Println("Cover art downloaded.")
|
||||
} else {
|
||||
fmt.Println("Could not download cover art.")
|
||||
}
|
||||
}
|
||||
err = tagFile(filename, ext, info, lyric, info.Cover)
|
||||
if err != nil {
|
||||
fmt.Printf("Error writing metadata: %v\n", err)
|
||||
fmt.Println("File is saved without metadata.")
|
||||
} else {
|
||||
fmt.Println("Metadata written successfully.")
|
||||
}
|
||||
fmt.Printf("Download complete! Saved to %s\n", filename)
|
||||
},
|
||||
}
|
||||
|
||||
// tagFile and its helpers (tagMp3, tagFlac) remain unchanged.
|
||||
func tagFile(filename, ext string, info miaosic.MediaInfo, lyric []miaosic.Lyrics, cover miaosic.Picture) error {
|
||||
switch strings.ToLower(ext) {
|
||||
case ".mp3":
|
||||
return tagwriter.WriteId3v2(filename, info, lyric, cover)
|
||||
case ".flac":
|
||||
return tagwriter.WriteFlac(filename, info, lyric, cover)
|
||||
default:
|
||||
return fmt.Errorf("unsupported file type for tagging: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func extensionFromContentType(ct string) (string, error) {
|
||||
switch ct {
|
||||
case "audio/mpeg":
|
||||
return ".mp3", nil
|
||||
case "audio/flac", "audio/x-flac":
|
||||
return ".flac", nil
|
||||
case "audio/mp4":
|
||||
return ".m4a", nil
|
||||
case "audio/aac":
|
||||
return ".aac", nil
|
||||
}
|
||||
return "", fmt.Errorf("unsupported content type: %s", ct)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package miaosic
|
||||
|
||||
const VERSION = "0.2.6"
|
||||
|
||||
type Picture struct {
|
||||
Url string `json:"url"`
|
||||
Data []byte `json:"data"`
|
||||
|
||||
67
tag/reader.go
Normal file
67
tag/reader.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"io"
|
||||
)
|
||||
|
||||
func Read(r io.ReadSeeker) (Metadata, error) {
|
||||
_, err := r.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
|
||||
b := make([]byte, 512)
|
||||
_, err = io.ReadFull(r, b)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
mimeType := mimetype.Detect(b).String()
|
||||
_, err = r.Seek(0, io.SeekStart)
|
||||
switch {
|
||||
case string(b[0:4]) == "fLaC":
|
||||
return ReadFLACTags(r, mimeType)
|
||||
//case string(b[0:4]) == "OggS":
|
||||
// return ReadOGGTags(r)
|
||||
//case string(b[4:8]) == "ftyp":
|
||||
// return ReadAtoms(r)
|
||||
case string(b[0:3]) == "ID3":
|
||||
return ReadID3v2Tags(r, mimeType)
|
||||
//case string(b[0:4]) == "DSD ":
|
||||
// return ReadDSFTags(r)
|
||||
}
|
||||
return fallbackRead(r, mimeType)
|
||||
}
|
||||
|
||||
func fallbackRead(r io.ReadSeeker, mime string) (Metadata, error) {
|
||||
meta := Metadata{
|
||||
Mimetype: mime,
|
||||
}
|
||||
m, err := tag.ReadFrom(r)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
meta.Format = string(m.Format())
|
||||
meta.Title = m.Title()
|
||||
meta.Artist = m.Artist()
|
||||
meta.Album = m.Album()
|
||||
meta.Lyrics = []Lyrics{}
|
||||
if m.Lyrics() != "" {
|
||||
meta.Lyrics = append(meta.Lyrics, Lyrics{
|
||||
Lang: "unk",
|
||||
Lyrics: m.Lyrics(),
|
||||
})
|
||||
}
|
||||
meta.Pictures = []Picture{}
|
||||
if m.Picture() != nil {
|
||||
p := m.Picture()
|
||||
meta.Pictures = append(meta.Pictures, Picture{
|
||||
Mimetype: p.MIMEType,
|
||||
Type: PictureTypeFrontCover,
|
||||
Description: p.Description,
|
||||
Data: p.Data,
|
||||
})
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
65
tag/reader_flac.go
Normal file
65
tag/reader_flac.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/go-flac/flacpicture/v2"
|
||||
"github.com/go-flac/flacvorbis/v2"
|
||||
"github.com/go-flac/go-flac/v2"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ReadFLACTags(r io.ReadSeeker, mime string) (Metadata, error) {
|
||||
meta := Metadata{
|
||||
Mimetype: mime,
|
||||
Format: FormatVORBIS,
|
||||
}
|
||||
metadata, err := flac.ParseMetadata(r)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
for _, block := range metadata.Meta {
|
||||
switch block.Type {
|
||||
case flac.VorbisComment:
|
||||
comment, err := flacvorbis.ParseFromMetaDataBlock(*block)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, tag := range comment.Comments {
|
||||
parts := strings.SplitN(tag, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToUpper(parts[0])
|
||||
value := parts[1]
|
||||
|
||||
switch key {
|
||||
case flacvorbis.FIELD_TITLE:
|
||||
meta.Title = value
|
||||
case flacvorbis.FIELD_ARTIST:
|
||||
meta.Artist = value
|
||||
case flacvorbis.FIELD_ALBUM:
|
||||
meta.Album = value
|
||||
case "LYRICS":
|
||||
meta.Lyrics = append(meta.Lyrics, Lyrics{
|
||||
Lang: "unk",
|
||||
Lyrics: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
case flac.Picture:
|
||||
pic, err := flacpicture.ParseFromMetaDataBlock(*block)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
meta.Pictures = append(meta.Pictures, Picture{
|
||||
Mimetype: pic.MIME,
|
||||
Type: byte(pic.PictureType),
|
||||
Description: pic.Description,
|
||||
Data: pic.ImageData,
|
||||
})
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
50
tag/reader_id3v2.go
Normal file
50
tag/reader_id3v2.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/bogem/id3v2/v2"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ReadID3v2Tags(r io.ReadSeeker, mime string) (Metadata, error) {
|
||||
meta := Metadata{
|
||||
Mimetype: mime,
|
||||
}
|
||||
tags, err := id3v2.ParseReader(r, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
if tags.Version() == 3 {
|
||||
meta.Format = FormatID3v2_3
|
||||
}
|
||||
if tags.Version() == 4 {
|
||||
meta.Format = FormatID3v2_4
|
||||
}
|
||||
meta.Title = tags.Title()
|
||||
meta.Artist = tags.Artist()
|
||||
meta.Album = tags.Album()
|
||||
meta.Lyrics = make([]Lyrics, 0)
|
||||
for _, frame := range tags.GetFrames("USLT") {
|
||||
lyricFrame, ok := frame.(id3v2.UnsynchronisedLyricsFrame)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
meta.Lyrics = append(meta.Lyrics, Lyrics{
|
||||
Lang: lyricFrame.Language,
|
||||
Lyrics: lyricFrame.Lyrics,
|
||||
})
|
||||
}
|
||||
meta.Pictures = make([]Picture, 0)
|
||||
for _, frame := range tags.GetFrames("APIC") {
|
||||
pic, ok := frame.(id3v2.PictureFrame)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
meta.Pictures = append(meta.Pictures, Picture{
|
||||
Mimetype: pic.MimeType,
|
||||
Type: pic.PictureType,
|
||||
Description: pic.Description,
|
||||
Data: pic.Picture,
|
||||
})
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
26
tag/reader_test.go
Normal file
26
tag/reader_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/k0kubun/pp/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
f, err := os.Open("/home/aynakeya/workspace/AynaLivePlayer/pkg/miaosic/cmd/miaosic/Mili - world.execute (me) ;.mp3")
|
||||
require.NoError(t, err)
|
||||
meta, err := Read(f)
|
||||
require.NoError(t, err)
|
||||
pp.Println(meta)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
func TestReader_Flac(t *testing.T) {
|
||||
f, err := os.Open("/home/aynakeya/workspace/AynaLivePlayer/pkg/miaosic/cmd/miaosic/欢子 - 心痛2009.flac")
|
||||
require.NoError(t, err)
|
||||
meta, err := Read(f)
|
||||
require.NoError(t, err)
|
||||
pp.Println(meta)
|
||||
f.Close()
|
||||
}
|
||||
4
tag/readme.md
Normal file
4
tag/readme.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Simple tag reader / writer
|
||||
|
||||
- https://id3.org/id3v2.3.0#ID3v2_header
|
||||
- https://www.xiph.org/vorbis/doc/v-comment.html
|
||||
28
tag/tag.go
28
tag/tag.go
@@ -1 +1,29 @@
|
||||
package tag
|
||||
|
||||
type Picture struct {
|
||||
Mimetype string
|
||||
Type byte
|
||||
Description string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (p Picture) TypeName() string {
|
||||
return pictureTypes[p.Type]
|
||||
}
|
||||
|
||||
type Lyrics struct {
|
||||
Lang string `json:"lang"`
|
||||
Lyrics string `json:"lyrics"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Format string `json:"format"`
|
||||
Mimetype string `json:"mimetype"`
|
||||
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
|
||||
Lyrics []Lyrics `json:"lyrics"`
|
||||
Pictures []Picture `json:"pictures"`
|
||||
}
|
||||
|
||||
57
tag/tag_const.go
Normal file
57
tag/tag_const.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package tag
|
||||
|
||||
const (
|
||||
FormatID3v2_2 = "ID3v2.2"
|
||||
FormatID3v2_3 = "ID3v2.3"
|
||||
FormatID3v2_4 = "ID3v2.4"
|
||||
FormatMP4 = "MP4"
|
||||
FormatVORBIS = "VORBIS"
|
||||
)
|
||||
|
||||
const (
|
||||
PictureTypeOther = iota
|
||||
PictureTypeFileIcon
|
||||
PictureTypeOtherFileIcon
|
||||
PictureTypeFrontCover
|
||||
PictureTypeBackCover
|
||||
PictureTypeLeafletPage
|
||||
PictureTypeMedia
|
||||
PictureTypeLeadArtistSoloist
|
||||
PictureTypeArtistPerformer
|
||||
PictureTypeConductor
|
||||
PictureTypeBandOrchestra
|
||||
PictureTypeComposer
|
||||
PictureTypeLyricistTextWriter
|
||||
PictureTypeRecordingLocation
|
||||
PictureTypeDuringRecording
|
||||
PictureTypeDuringPerformance
|
||||
PictureTypeMovieScreenCaPictureTypeure
|
||||
PictureTypeBrightColouredFish
|
||||
PictureTypeIllustration
|
||||
PictureTypeBandArtistLogotype
|
||||
PictureTypePublisherStudioLogotype
|
||||
)
|
||||
|
||||
var pictureTypes = map[byte]string{
|
||||
0x00: "Other",
|
||||
0x01: "32x32 pixels 'file icon' (PNG only)",
|
||||
0x02: "Other file icon",
|
||||
0x03: "Cover (front)",
|
||||
0x04: "Cover (back)",
|
||||
0x05: "Leaflet page",
|
||||
0x06: "Media (e.g. lable side of CD)",
|
||||
0x07: "Lead artist/lead performer/soloist",
|
||||
0x08: "Artist/performer",
|
||||
0x09: "Conductor",
|
||||
0x0A: "Band/Orchestra",
|
||||
0x0B: "Composer",
|
||||
0x0C: "Lyricist/text writer",
|
||||
0x0D: "Recording Location",
|
||||
0x0E: "During recording",
|
||||
0x0F: "During performance",
|
||||
0x10: "Movie/video screen capture",
|
||||
0x11: "A bright coloured fish",
|
||||
0x12: "Illustration",
|
||||
0x13: "Band/artist logotype",
|
||||
0x14: "Publisher/Studio logotype",
|
||||
}
|
||||
55
tag/writer.go
Normal file
55
tag/writer.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func fixMeta(meta *Metadata) {
|
||||
// fix picture meme
|
||||
for idx, _ := range meta.Pictures {
|
||||
if meta.Pictures[idx].Mimetype == "" {
|
||||
meta.Pictures[idx].Mimetype = mimetype.Detect(meta.Pictures[idx].Data).String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write metadata to file, input file will be closed after this method
|
||||
func Write(f *os.File, meta Metadata) error {
|
||||
_, err := f.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := make([]byte, 512)
|
||||
_, err = io.ReadFull(f, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mimeType := mimetype.Detect(b).String()
|
||||
_, err = f.Seek(0, io.SeekStart)
|
||||
fixMeta(&meta)
|
||||
switch mimeType {
|
||||
case "audio/flac", "audio/x-flac":
|
||||
return WriteFlacTags(f, meta)
|
||||
//case string(b[0:4]) == "OggS":
|
||||
// return ReadOGGTags(r)
|
||||
//case string(b[4:8]) == "ftyp":
|
||||
// return ReadAtoms(r)
|
||||
case "audio/mpeg":
|
||||
return WriteID3v2Tags(f, meta)
|
||||
//case string(b[0:4]) == "DSD ":
|
||||
// return ReadDSFTags(r)
|
||||
}
|
||||
return errors.New("miaosic: mime-type not supported")
|
||||
}
|
||||
|
||||
func WriteTo(path string, meta Metadata) error {
|
||||
file, err := os.Open(path)
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Write(file, meta)
|
||||
}
|
||||
85
tag/writer_flac.go
Normal file
85
tag/writer_flac.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
"github.com/go-flac/flacpicture/v2"
|
||||
"github.com/go-flac/flacvorbis/v2"
|
||||
"github.com/go-flac/go-flac/v2"
|
||||
"os"
|
||||
)
|
||||
|
||||
type posMetaBlock[T any] struct {
|
||||
block T
|
||||
idx int
|
||||
}
|
||||
|
||||
func WriteFlacTags(f *os.File, meta Metadata) error {
|
||||
flacFile, err := flac.ParseBytes(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing flac file: %w", err)
|
||||
}
|
||||
var commentBlock posMetaBlock[*flacvorbis.MetaDataBlockVorbisComment]
|
||||
var pictures = map[byte]posMetaBlock[*flacpicture.MetadataBlockPicture]{}
|
||||
var pic *flacpicture.MetadataBlockPicture
|
||||
var cmt *flacvorbis.MetaDataBlockVorbisComment
|
||||
for idx, metaBlock := range flacFile.Meta {
|
||||
if metaBlock.Type == flac.VorbisComment {
|
||||
cmt, err = flacvorbis.ParseFromMetaDataBlock(*metaBlock)
|
||||
if err == nil {
|
||||
commentBlock = posMetaBlock[*flacvorbis.MetaDataBlockVorbisComment]{
|
||||
block: cmt,
|
||||
idx: idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
if metaBlock.Type == flac.Picture {
|
||||
pic, err = flacpicture.ParseFromMetaDataBlock(*metaBlock)
|
||||
if err == nil {
|
||||
pictures[byte(pic.PictureType)] = posMetaBlock[*flacpicture.MetadataBlockPicture]{
|
||||
block: pic,
|
||||
idx: idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// write comment, include basic info and lyrcis
|
||||
commentBlockExists := true
|
||||
if commentBlock.block == nil {
|
||||
commentBlock.block = &flacvorbis.MetaDataBlockVorbisComment{
|
||||
Comments: []string{},
|
||||
}
|
||||
commentBlockExists = false
|
||||
}
|
||||
// just reset all
|
||||
commentBlock.block.Vendor = "miaosic" + miaosic.VERSION
|
||||
commentBlock.block.Comments = []string{}
|
||||
_ = commentBlock.block.Add(flacvorbis.FIELD_TITLE, meta.Title)
|
||||
_ = commentBlock.block.Add(flacvorbis.FIELD_ARTIST, meta.Artist)
|
||||
_ = commentBlock.block.Add(flacvorbis.FIELD_ALBUM, meta.Album)
|
||||
for _, lyric := range meta.Lyrics {
|
||||
_ = commentBlock.block.Add("LYRICS", lyric.Lyrics)
|
||||
}
|
||||
commentBlockMeta := commentBlock.block.Marshal()
|
||||
if commentBlockExists {
|
||||
flacFile.Meta[commentBlock.idx] = &commentBlockMeta
|
||||
} else {
|
||||
flacFile.Meta = append(flacFile.Meta, &commentBlockMeta)
|
||||
}
|
||||
// write file
|
||||
for _, picture := range meta.Pictures {
|
||||
newPic, err := flacpicture.NewFromImageData(flacpicture.PictureType(picture.Type),
|
||||
picture.Description, picture.Data, picture.Mimetype)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
picBlock, ok := pictures[picture.Type]
|
||||
picBlockMeta := newPic.Marshal()
|
||||
if ok {
|
||||
flacFile.Meta[picBlock.idx] = &picBlockMeta
|
||||
} else {
|
||||
flacFile.Meta = append(flacFile.Meta, &picBlockMeta)
|
||||
}
|
||||
}
|
||||
return flacFile.Save(f.Name())
|
||||
}
|
||||
15
tag/writer_flac_test.go
Normal file
15
tag/writer_flac_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriterFlac(t *testing.T) {
|
||||
f, err := os.Open("/home/aynakeya/workspace/AynaLivePlayer/pkg/miaosic/cmd/miaosic/data.flac")
|
||||
require.NoError(t, err)
|
||||
err = WriteFlacTags(f, Metadata{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
}
|
||||
26
tag/writer_id3v2.go
Normal file
26
tag/writer_id3v2.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bogem/id3v2/v2"
|
||||
"os"
|
||||
)
|
||||
|
||||
func WriteID3v2Tags(f *os.File, meta Metadata) error {
|
||||
tag, err := id3v2.ParseReader(f, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing mp3 file: %w", err)
|
||||
}
|
||||
tag.SetTitle(meta.Title)
|
||||
tag.SetArtist(meta.Artist)
|
||||
tag.SetAlbum(meta.Album)
|
||||
for _, lyric := range meta.Lyrics {
|
||||
uslf := id3v2.UnsynchronisedLyricsFrame{Encoding: id3v2.EncodingUTF8, Language: lyric.Lang[:min(3, len(lyric.Lang))], Lyrics: lyric.Lyrics}
|
||||
tag.AddUnsynchronisedLyricsFrame(uslf)
|
||||
}
|
||||
for _, pic := range meta.Pictures {
|
||||
picFrame := id3v2.PictureFrame{Encoding: id3v2.EncodingUTF8, MimeType: pic.Mimetype, PictureType: pic.Type, Description: pic.Description, Picture: pic.Data}
|
||||
tag.AddAttachedPicture(picFrame)
|
||||
}
|
||||
return tag.Save()
|
||||
}
|
||||
Reference in New Issue
Block a user