Add mp3 support + command line option to limit tracks + general redesign for more modularity
This commit is contained in:
		
							
								
								
									
										113
									
								
								mp3/mp3.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								mp3/mp3.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
package mp3
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"hash/crc32"
 | 
			
		||||
	"html"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrNoMetaint         = errors.New("mp3: key 'icy-metaint' not found in HTTP header")
 | 
			
		||||
	ErrCorruptedMetadata = errors.New("mp3: corrupted metadata")
 | 
			
		||||
	ErrNoStreamTitle     = errors.New("mp3: no 'StreamTitle' tag in metadata")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Extractor struct {
 | 
			
		||||
	metaint        int64 // Distance between two metadata chunks
 | 
			
		||||
	hasStreamTitle bool
 | 
			
		||||
	streamTitle    string // Metadata tag determining the filename
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewExtractor(respHdr http.Header) (*Extractor, error) {
 | 
			
		||||
	mi := respHdr.Get("icy-metaint")
 | 
			
		||||
	if mi == "" {
 | 
			
		||||
		return nil, ErrNoMetaint
 | 
			
		||||
	}
 | 
			
		||||
	miNum, _ := strconv.ParseInt(mi, 10, 64)
 | 
			
		||||
	return &Extractor{
 | 
			
		||||
		metaint: miNum,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Extractor) ReadBlock(r io.Reader, w io.Writer) (isFirst bool, err error) {
 | 
			
		||||
	var musicData bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	// We want to write everything to the output, as well as musicData for
 | 
			
		||||
	// calculating the checksum.
 | 
			
		||||
	multi := io.MultiWriter(w, &musicData)
 | 
			
		||||
 | 
			
		||||
	// Read until the metadata chunk. The part that is read here is also what
 | 
			
		||||
	// contains the actual mp3 music data.
 | 
			
		||||
	io.CopyN(multi, r, d.metaint)
 | 
			
		||||
 | 
			
		||||
	// Read number of metadata blocks (blocks within this function are not what
 | 
			
		||||
	// is meant with `ReadBlock()`).
 | 
			
		||||
	var numBlocks uint8
 | 
			
		||||
	err = binary.Read(r, binary.LittleEndian, &numBlocks)
 | 
			
		||||
 | 
			
		||||
	// Whether this block is the beginning of a new track.
 | 
			
		||||
	var isBOF bool
 | 
			
		||||
 | 
			
		||||
	// Read metadata blocks.
 | 
			
		||||
	if numBlocks > 0 {
 | 
			
		||||
		// Metadata is only actually stored in the first metadata chunk
 | 
			
		||||
		// of a given file. Therefore, every metadata chunk with more than 1
 | 
			
		||||
		// block always marks the beginning of a file.
 | 
			
		||||
		isBOF = true
 | 
			
		||||
 | 
			
		||||
		// Each block is 16 bytes in size. Any excess bytes in the last block
 | 
			
		||||
		// are set to '\0', which is great because the `string()` conversion
 | 
			
		||||
		// function ignores null bytes. The whole string is escaped via HTML.
 | 
			
		||||
		// Metadata format: k0='v0';k1='v1';
 | 
			
		||||
		raw := make([]byte, numBlocks*16)
 | 
			
		||||
		if _, err := r.Read(raw); err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
		rawString := html.UnescapeString(string(raw))
 | 
			
		||||
		for _, data := range strings.Split(rawString, ";") {
 | 
			
		||||
			s := strings.Split(data, "=")
 | 
			
		||||
			if len(s) == 2 {
 | 
			
		||||
				if s[0] == "StreamTitle" {
 | 
			
		||||
					d.hasStreamTitle = true
 | 
			
		||||
					// Strip stream title's first and last character (single
 | 
			
		||||
					// quotes).
 | 
			
		||||
					t := s[1]
 | 
			
		||||
					if len(t) < 2 {
 | 
			
		||||
						return false, ErrCorruptedMetadata
 | 
			
		||||
					}
 | 
			
		||||
					t = t[1 : len(t)-1]
 | 
			
		||||
					if t == "Unknown" {
 | 
			
		||||
						// If there is no stream title, use format:
 | 
			
		||||
						// Unknown_<crc32 checksum>
 | 
			
		||||
						// Where the checksum is only that of the first block.
 | 
			
		||||
						sumStr := strconv.FormatInt(int64(crc32.ChecksumIEEE(musicData.Bytes())), 10)
 | 
			
		||||
						d.streamTitle = "Unknown_" + sumStr
 | 
			
		||||
					} else {
 | 
			
		||||
						d.streamTitle = t
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else if len(s) != 1 {
 | 
			
		||||
				return false, ErrCorruptedMetadata
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !d.hasStreamTitle {
 | 
			
		||||
			return false, ErrNoStreamTitle
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return isBOF, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Extractor) TryGetFilename() (filename string, hasFilename bool) {
 | 
			
		||||
	if !d.hasStreamTitle {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
	base := strings.ReplaceAll(d.streamTitle, "/", "_") // Replace invalid characters.
 | 
			
		||||
	return base + ".mp3", true
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user