2653586eea
* Add download queue tracking and UI integration Introduces backend support for a download queue with item tracking, status updates, and session statistics. Adds frontend components and hooks for displaying and managing the download queue, including a dialog and toast indicator. Updates download logic to pre-add items to the queue, track progress, and handle completion, skipping, and failure states. Integrates @radix-ui/react-scroll-area for improved UI scrolling. * Add session stats to DownloadQueue dialog Introduces session statistics (downloaded amount, speed, and duration) to the DownloadQueue dialog for improved user feedback. Also adjusts dialog sizing for better display and removes the sm:max-w-lg restriction in dialog.tsx for more flexible width.
190 lines
4.2 KiB
Go
190 lines
4.2 KiB
Go
package backend
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-flac/flacpicture"
|
|
"github.com/go-flac/flacvorbis"
|
|
"github.com/go-flac/go-flac"
|
|
)
|
|
|
|
type Metadata struct {
|
|
Title string
|
|
Artist string
|
|
Album string
|
|
Date string
|
|
TrackNumber int
|
|
DiscNumber int
|
|
ISRC string
|
|
}
|
|
|
|
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
|
f, err := flac.ParseFile(filepath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse FLAC file: %w", err)
|
|
}
|
|
|
|
var cmtIdx = -1
|
|
for idx, block := range f.Meta {
|
|
if block.Type == flac.VorbisComment {
|
|
cmtIdx = idx
|
|
break
|
|
}
|
|
}
|
|
|
|
cmt := flacvorbis.New()
|
|
|
|
if metadata.Title != "" {
|
|
_ = cmt.Add(flacvorbis.FIELD_TITLE, metadata.Title)
|
|
}
|
|
if metadata.Artist != "" {
|
|
_ = cmt.Add(flacvorbis.FIELD_ARTIST, metadata.Artist)
|
|
}
|
|
if metadata.Album != "" {
|
|
_ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album)
|
|
}
|
|
if metadata.Date != "" {
|
|
_ = cmt.Add(flacvorbis.FIELD_DATE, metadata.Date)
|
|
}
|
|
if metadata.TrackNumber > 0 {
|
|
_ = cmt.Add(flacvorbis.FIELD_TRACKNUMBER, strconv.Itoa(metadata.TrackNumber))
|
|
}
|
|
if metadata.DiscNumber > 0 {
|
|
_ = cmt.Add("DISCNUMBER", strconv.Itoa(metadata.DiscNumber))
|
|
}
|
|
if metadata.ISRC != "" {
|
|
_ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC)
|
|
}
|
|
|
|
cmtBlock := cmt.Marshal()
|
|
if cmtIdx < 0 {
|
|
f.Meta = append(f.Meta, &cmtBlock)
|
|
} else {
|
|
f.Meta[cmtIdx] = &cmtBlock
|
|
}
|
|
|
|
if coverPath != "" && fileExists(coverPath) {
|
|
if err := embedCoverArt(f, coverPath); err != nil {
|
|
fmt.Printf("Warning: Failed to embed cover art: %v\n", err)
|
|
}
|
|
}
|
|
|
|
if err := f.Save(filepath); err != nil {
|
|
return fmt.Errorf("failed to save FLAC file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func embedCoverArt(f *flac.File, coverPath string) error {
|
|
imgData, err := os.ReadFile(coverPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read cover image: %w", err)
|
|
}
|
|
|
|
picture, err := flacpicture.NewFromImageData(
|
|
flacpicture.PictureTypeFrontCover,
|
|
"Cover",
|
|
imgData,
|
|
"image/jpeg",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create picture block: %w", err)
|
|
}
|
|
|
|
pictureBlock := picture.Marshal()
|
|
|
|
for i := len(f.Meta) - 1; i >= 0; i-- {
|
|
if f.Meta[i].Type == flac.Picture {
|
|
f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
|
|
}
|
|
}
|
|
|
|
f.Meta = append(f.Meta, &pictureBlock)
|
|
|
|
return nil
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
// ReadISRCFromFile reads ISRC metadata from a FLAC file
|
|
func ReadISRCFromFile(filepath string) (string, error) {
|
|
if !fileExists(filepath) {
|
|
return "", fmt.Errorf("file does not exist")
|
|
}
|
|
|
|
f, err := flac.ParseFile(filepath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse FLAC file: %w", err)
|
|
}
|
|
|
|
// Find VorbisComment block
|
|
for _, block := range f.Meta {
|
|
if block.Type == flac.VorbisComment {
|
|
cmt, err := flacvorbis.ParseFromMetaDataBlock(*block)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Get ISRC field
|
|
isrcValues, err := cmt.Get(flacvorbis.FIELD_ISRC)
|
|
if err == nil && len(isrcValues) > 0 {
|
|
return isrcValues[0], nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", nil // No ISRC found
|
|
}
|
|
|
|
// CheckISRCExists checks if a file with the given ISRC already exists in the directory
|
|
func CheckISRCExists(outputDir string, targetISRC string) (string, bool) {
|
|
if targetISRC == "" {
|
|
return "", false
|
|
}
|
|
|
|
// Read all .flac files in directory
|
|
entries, err := os.ReadDir(outputDir)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Check only .flac files
|
|
filename := entry.Name()
|
|
if len(filename) < 5 || filename[len(filename)-5:] != ".flac" {
|
|
continue
|
|
}
|
|
|
|
filepath := fmt.Sprintf("%s/%s", outputDir, filename)
|
|
|
|
// Read ISRC from file (this will fail for corrupted files)
|
|
isrc, err := ReadISRCFromFile(filepath)
|
|
if err != nil {
|
|
// File is corrupted or unreadable, delete it
|
|
fmt.Printf("Removing corrupted/unreadable file: %s (error: %v)\n", filepath, err)
|
|
if removeErr := os.Remove(filepath); removeErr != nil {
|
|
fmt.Printf("Warning: Failed to remove corrupted file %s: %v\n", filepath, removeErr)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Compare ISRC (case-insensitive)
|
|
if isrc != "" && strings.EqualFold(isrc, targetISRC) {
|
|
return filepath, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|