main
Uriel Fanelli 2025-05-03 14:05:30 +02:00
parent d9a2ced2fb
commit 641590e179
24 changed files with 3343 additions and 7 deletions

36
Dockerfile Normal file
View File

@ -0,0 +1,36 @@
# Fase 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copia i file necessari per la compilazione
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Compila l'applicazione
RUN CGO_ENABLED=0 GOOS=linux go build -o u-pod
# Fase 2: Container leggero
FROM alpine:latest
WORKDIR /app
# Copia solo l'eseguibile dalla fase di build
COPY --from=builder /app/u-pod .
COPY --from=builder /app/cover.jpg .
RUN mkdir -p audio covers
# Variabili d'ambiente obbligatorie (da sovrascrivere al runtime)
ENV PODCAST_PORT=":8080" \
PODCAST_BASE_URL="http://localhost:8080" \
PODCAST_AUDIO_DIR="/app/audio" \
PODCAST_COVERS_DIR="/app/covers" \
PODCAST_TITLE="Yo mom Podcast"
# Esponi la porta e avvia l'applicazione
EXPOSE 8080
VOLUME ["/app/audio", "/app/covers"]
CMD ["./u-pod"]

View File

@ -1,16 +1,26 @@
# U-Pod
## Overview
This is U-Pod, the torpedo-style podcast system with minimal footprint and maximum utility.
No databases needed to function, no cloud required, no datacenter necessary - just the filesystem and your content. Even your mom on her knees could help set it up, though that's not a functional requirement.
## Setup
First, you need to build the image. I've included a handy Dockerfile in the folder. Use it.
Once you've created your image, use Docker to run it after setting these environment variables:
<pre>
PODCAST_PORT=:8080
PODCAST_BASE_URL=http://your-domain.com
PODCAST_AUDIO_DIR=/app/data/audio
PODCAST_COVERS_DIR=/app/data/covers
PODCAST_TITLE="My Podcast - and your mom"
</pre>
## File Linking
What links files in /audio to files in /covers?
@ -28,19 +38,19 @@ U-Pod will then create the torpedo, with the image properly linked to the audio.
Once you have the Docker image, simply create the /data folder and put your files in it.
FAQ:
## FAQ
- My "SisterGermanaGangbang 3.0" software can't use it.
- It uses the standard podcast format http://www.itunes.com/dtds/podcast-1.0.dtd. If your shitty software can't read it, take it up with Sister Germana.
- It uses the standard podcast format http://www.itunes.com/dtds/podcast-1.0.dtd. If your shitty software can't read it, take it up with Sister Germana.
- I don't know how to create Docker images.
- Learn.
- Learn.
- What do you mean by environment variable?
- Die! Die! Die!
- Die! Die! Die!
- I need you to change this, this and that.
- No, No, and No
- No, No, and No
- How can I get support for this software?
- You cannot. Code is simple, and you are alone in the night, like any operation engineer during the night. Man up!!!!

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module u-pod
go 1.21.0
require github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=

72
main.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
var (
port = os.Getenv("PODCAST_PORT") // Es: ":8080"
baseURL = os.Getenv("PODCAST_BASE_URL") // Es: "http://mioserver.com"
audioDir = os.Getenv("PODCAST_AUDIO_DIR") // Es: "/data/podcast/audio"
coversDir = os.Getenv("PODCAST_COVERS_DIR") // Es: "/data/podcast/covers"
podTitle = os.Getenv("PODCAST_TITLE") // Es: "Il Mio Podcast"
)
func init() {
// Verifica variabili d'ambiente
for _, v := range []struct {
name string
value string
}{
{"PODCAST_PORT", port},
{"PODCAST_BASE_URL", baseURL},
{"PODCAST_AUDIO_DIR", audioDir},
{"PODCAST_COVERS_DIR", coversDir},
{"PODCAST_TITLE", podTitle},
} {
if v.value == "" {
log.Fatalf("Variabile d'ambiente mancante: %s", v.name)
}
}
}
func main() {
// Handler principale
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Verifica se la richiesta è per un file audio o cover
requestedFile := filepath.Clean(r.URL.Path)
isAudio := strings.HasPrefix(requestedFile, "/audio/")
isCover := strings.HasPrefix(requestedFile, "/covers/")
if isAudio || isCover {
// Servi il file richiesto
http.ServeFile(w, r, filepath.Join(".", requestedFile))
return
}
// Altrimenti servi sempre l'RSS
if err := generateRSS(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, "feed.xml")
})
// Aggiornamento periodico
go func() {
for {
time.Sleep(5 * time.Minute)
if err := generateRSS(); err != nil {
log.Printf("Aggiornamento RSS fallito: %v", err)
}
}
}()
log.Printf("Server in ascolto su %s%s", baseURL, port)
log.Fatal(http.ListenAndServe(port, nil))
}

104
pod.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/dhowden/tag"
)
type Episode struct {
Title string
File string
Cover string
PubDate string
Size int64
}
var (
lastMod time.Time
rssLock sync.Mutex
)
func generateRSS() error {
rssLock.Lock()
defer rssLock.Unlock()
episodes, err := scanEpisodes()
if err != nil {
return err
}
rss := fmt.Sprintf(`<?xml version="1.0"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
<title>%s</title>
<link>%s</link>`, podTitle, baseURL)
for _, ep := range episodes {
rss += fmt.Sprintf(`
<item>
<title>%s</title>
<enclosure url="%s/%s" type="audio/mpeg" length="%d"/>
<pubDate>%s</pubDate>`,
ep.Title, baseURL, ep.File, ep.Size, ep.PubDate)
if ep.Cover != "" {
rss += fmt.Sprintf(`
<itunes:image href="%s/%s"/>`, baseURL, ep.Cover)
}
rss += "\n </item>"
}
rss += "\n</channel>\n</rss>"
return os.WriteFile("feed.xml", []byte(rss), 0644)
}
func scanEpisodes() ([]Episode, error) {
var episodes []Episode
err := filepath.Walk(audioDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".mp3") {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
meta, err := tag.ReadFrom(file)
if err != nil {
log.Printf("File %s senza metadati ID3: %v", path, err)
return nil
}
relPath, _ := filepath.Rel(".", path)
baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
coverPath := filepath.Join(coversDir, baseName+".jpg")
ep := Episode{
Title: meta.Title(),
File: relPath,
PubDate: info.ModTime().Format(time.RFC1123),
Size: info.Size(),
}
if _, err := os.Stat(coverPath); err == nil {
ep.Cover, _ = filepath.Rel(".", coverPath)
}
episodes = append(episodes, ep)
return nil
})
return episodes, err
}

19
vendor/github.com/dhowden/tag/.editorconfig generated vendored Normal file
View File

@ -0,0 +1,19 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*.go]
indent_style = tab
indent_size = 3
[*.md]
trim_trailing_whitespace = false
[*]
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

5
vendor/github.com/dhowden/tag/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,5 @@
language: go
go:
- 1.7
- tip

23
vendor/github.com/dhowden/tag/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright 2015, David Howden
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

71
vendor/github.com/dhowden/tag/README.md generated vendored Normal file
View File

@ -0,0 +1,71 @@
# MP3/MP4/OGG/FLAC metadata parsing library
[![GoDoc](https://pkg.go.dev/badge/github.com/dhowden/tag)](https://pkg.go.dev/github.com/dhowden/tag)
This package provides MP3 (ID3v1,2.{2,3,4}) and MP4 (ACC, M4A, ALAC), OGG and FLAC metadata detection, parsing and artwork extraction.
Detect and parse tag metadata from an `io.ReadSeeker` (i.e. an `*os.File`):
```go
m, err := tag.ReadFrom(f)
if err != nil {
log.Fatal(err)
}
log.Print(m.Format()) // The detected format.
log.Print(m.Title()) // The title of the track (see Metadata interface for more details).
```
Parsed metadata is exported via a single interface (giving a consistent API for all supported metadata formats).
```go
// Metadata is an interface which is used to describe metadata retrieved by this package.
type Metadata interface {
Format() Format
FileType() FileType
Title() string
Album() string
Artist() string
AlbumArtist() string
Composer() string
Genre() string
Year() int
Track() (int, int) // Number, Total
Disc() (int, int) // Number, Total
Picture() *Picture // Artwork
Lyrics() string
Comment() string
Raw() map[string]interface{} // NB: raw tag names are not consistent across formats.
}
```
## Audio Data Checksum (SHA1)
This package also provides a metadata-invariant checksum for audio files: only the audio data is used to
construct the checksum.
[https://pkg.go.dev/github.com/dhowden/tag#Sum](https://pkg.go.dev/github.com/dhowden/tag#Sum)
## Tools
There are simple command-line tools which demonstrate basic tag extraction and summing:
```console
$ go install github.com/dhowden/tag/cmd/tag@latest
$ cd $GOPATH/bin
$ ./tag 11\ High\ Hopes.m4a
Metadata Format: MP4
Title: High Hopes
Album: The Division Bell
Artist: Pink Floyd
Composer: Abbey Road Recording Studios/David Gilmour/Polly Samson
Year: 1994
Track: 11 of 11
Disc: 1 of 1
Picture: Picture{Ext: jpeg, MIMEType: image/jpeg, Type: , Description: , Data.Size: 606109}
$ ./sum 11\ High\ Hopes.m4a
2ae208c5f00a1f21f5fac9b7f6e0b8e52c06da29
```

109
vendor/github.com/dhowden/tag/dsf.go generated vendored Normal file
View File

@ -0,0 +1,109 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"errors"
"io"
)
// ReadDSFTags reads DSF metadata from the io.ReadSeeker, returning the resulting
// metadata in a Metadata implementation, or non-nil error if there was a problem.
// samples: http://www.2l.no/hires/index.html
func ReadDSFTags(r io.ReadSeeker) (Metadata, error) {
dsd, err := readString(r, 4)
if err != nil {
return nil, err
}
if dsd != "DSD " {
return nil, errors.New("expected 'DSD '")
}
_, err = r.Seek(int64(16), io.SeekCurrent)
if err != nil {
return nil, err
}
id3Pointer, err := readUint64LittleEndian(r)
if err != nil {
return nil, err
}
_, err = r.Seek(int64(id3Pointer), io.SeekStart)
if err != nil {
return nil, err
}
id3, err := ReadID3v2Tags(r)
if err != nil {
return nil, err
}
return metadataDSF{id3}, nil
}
type metadataDSF struct {
id3 Metadata
}
func (m metadataDSF) Format() Format {
return m.id3.Format()
}
func (m metadataDSF) FileType() FileType {
return DSF
}
func (m metadataDSF) Title() string {
return m.id3.Title()
}
func (m metadataDSF) Album() string {
return m.id3.Album()
}
func (m metadataDSF) Artist() string {
return m.id3.Artist()
}
func (m metadataDSF) AlbumArtist() string {
return m.id3.AlbumArtist()
}
func (m metadataDSF) Composer() string {
return m.id3.Composer()
}
func (m metadataDSF) Year() int {
return m.id3.Year()
}
func (m metadataDSF) Genre() string {
return m.id3.Genre()
}
func (m metadataDSF) Track() (int, int) {
return m.id3.Track()
}
func (m metadataDSF) Disc() (int, int) {
return m.id3.Disc()
}
func (m metadataDSF) Picture() *Picture {
return m.id3.Picture()
}
func (m metadataDSF) Lyrics() string {
return m.id3.Lyrics()
}
func (m metadataDSF) Comment() string {
return m.id3.Comment()
}
func (m metadataDSF) Raw() map[string]interface{} {
return m.id3.Raw()
}

89
vendor/github.com/dhowden/tag/flac.go generated vendored Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"errors"
"io"
)
// blockType is a type which represents an enumeration of valid FLAC blocks
type blockType byte
// FLAC block types.
const (
// Stream Info Block 0
// Padding Block 1
// Application Block 2
// Seektable Block 3
// Cue Sheet Block 5
vorbisCommentBlock blockType = 4
pictureBlock blockType = 6
)
// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting
// metadata in a Metadata implementation, or non-nil error if there was a problem.
func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
flac, err := readString(r, 4)
if err != nil {
return nil, err
}
if flac != "fLaC" {
return nil, errors.New("expected 'fLaC'")
}
m := &metadataFLAC{
newMetadataVorbis(),
}
for {
last, err := m.readFLACMetadataBlock(r)
if err != nil {
return nil, err
}
if last {
break
}
}
return m, nil
}
type metadataFLAC struct {
*metadataVorbis
}
func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
blockHeader, err := readBytes(r, 1)
if err != nil {
return
}
if getBit(blockHeader[0], 7) {
blockHeader[0] ^= (1 << 7)
last = true
}
blockLen, err := readInt(r, 3)
if err != nil {
return
}
switch blockType(blockHeader[0]) {
case vorbisCommentBlock:
err = m.readVorbisComment(r)
case pictureBlock:
err = m.readPictureBlock(r)
default:
_, err = r.Seek(int64(blockLen), io.SeekCurrent)
}
return
}
func (m *metadataFLAC) FileType() FileType {
return FLAC
}

81
vendor/github.com/dhowden/tag/id.go generated vendored Normal file
View File

@ -0,0 +1,81 @@
package tag
import (
"fmt"
"io"
)
// Identify identifies the format and file type of the data in the ReadSeeker.
func Identify(r io.ReadSeeker) (format Format, fileType FileType, err error) {
b, err := readBytes(r, 11)
if err != nil {
return
}
_, err = r.Seek(-11, io.SeekCurrent)
if err != nil {
err = fmt.Errorf("could not seek back to original position: %v", err)
return
}
switch {
case string(b[0:4]) == "fLaC":
return VORBIS, FLAC, nil
case string(b[0:4]) == "OggS":
return VORBIS, OGG, nil
case string(b[4:8]) == "ftyp":
b = b[8:11]
fileType = UnknownFileType
switch string(b) {
case "M4A":
fileType = M4A
case "M4B":
fileType = M4B
case "M4P":
fileType = M4P
}
return MP4, fileType, nil
case string(b[0:3]) == "ID3":
b := b[3:]
switch uint(b[0]) {
case 2:
format = ID3v2_2
case 3:
format = ID3v2_3
case 4:
format = ID3v2_4
case 0, 1:
fallthrough
default:
err = fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
return
}
return format, MP3, nil
}
n, err := r.Seek(-128, io.SeekEnd)
if err != nil {
return
}
tag, err := readString(r, 3)
if err != nil {
return
}
_, err = r.Seek(-n, io.SeekCurrent)
if err != nil {
return
}
if tag != "TAG" {
err = ErrNoTagsFound
return
}
return ID3v1, MP3, nil
}

144
vendor/github.com/dhowden/tag/id3v1.go generated vendored Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"errors"
"io"
"strconv"
"strings"
)
// id3v1Genres is a list of genres as given in the ID3v1 specification.
var id3v1Genres = [...]string{
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
"Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
"Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
"Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
"Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
"Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
"Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
"Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
"Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
"Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
"Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
"National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
"Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
"Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall",
}
// ErrNotID3v1 is an error which is returned when no ID3v1 header is found.
var ErrNotID3v1 = errors.New("invalid ID3v1 header")
// ReadID3v1Tags reads ID3v1 tags from the io.ReadSeeker. Returns ErrNotID3v1
// if there are no ID3v1 tags, otherwise non-nil error if there was a problem.
func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) {
_, err := r.Seek(-128, io.SeekEnd)
if err != nil {
return nil, err
}
if tag, err := readString(r, 3); err != nil {
return nil, err
} else if tag != "TAG" {
return nil, ErrNotID3v1
}
title, err := readString(r, 30)
if err != nil {
return nil, err
}
artist, err := readString(r, 30)
if err != nil {
return nil, err
}
album, err := readString(r, 30)
if err != nil {
return nil, err
}
year, err := readString(r, 4)
if err != nil {
return nil, err
}
commentBytes, err := readBytes(r, 30)
if err != nil {
return nil, err
}
var comment string
var track int
if commentBytes[28] == 0 {
comment = trimString(string(commentBytes[:28]))
track = int(commentBytes[29])
} else {
comment = trimString(string(commentBytes))
}
var genre string
genreID, err := readBytes(r, 1)
if err != nil {
return nil, err
}
if int(genreID[0]) < len(id3v1Genres) {
genre = id3v1Genres[int(genreID[0])]
}
m := make(map[string]interface{})
m["title"] = trimString(title)
m["artist"] = trimString(artist)
m["album"] = trimString(album)
m["year"] = trimString(year)
m["comment"] = trimString(comment)
m["track"] = track
m["genre"] = genre
return metadataID3v1(m), nil
}
func trimString(x string) string {
return strings.TrimSpace(strings.Trim(x, "\x00"))
}
// metadataID3v1 is the implementation of Metadata used for ID3v1 tags.
type metadataID3v1 map[string]interface{}
func (metadataID3v1) Format() Format { return ID3v1 }
func (metadataID3v1) FileType() FileType { return MP3 }
func (m metadataID3v1) Raw() map[string]interface{} { return m }
func (m metadataID3v1) Title() string { return m["title"].(string) }
func (m metadataID3v1) Album() string { return m["album"].(string) }
func (m metadataID3v1) Artist() string { return m["artist"].(string) }
func (m metadataID3v1) Genre() string { return m["genre"].(string) }
func (m metadataID3v1) Year() int {
y := m["year"].(string)
n, err := strconv.Atoi(y)
if err != nil {
return 0
}
return n
}
func (m metadataID3v1) Track() (int, int) { return m["track"].(int), 0 }
func (m metadataID3v1) AlbumArtist() string { return "" }
func (m metadataID3v1) Composer() string { return "" }
func (metadataID3v1) Disc() (int, int) { return 0, 0 }
func (m metadataID3v1) Picture() *Picture { return nil }
func (m metadataID3v1) Lyrics() string { return "" }
func (m metadataID3v1) Comment() string { return m["comment"].(string) }

469
vendor/github.com/dhowden/tag/id3v2.go generated vendored Normal file
View File

@ -0,0 +1,469 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
// Genre definitions 0-79 follow the ID3 tag specification of 1999.
// More genres have been successively introduced in later Winamp versions.
// https://en.wikipedia.org/wiki/List_of_ID3v1_genres#Extension_by_Winamp
var id3v2Genres = [...]string{
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
"Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
"Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
"Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
"Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
"Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
"Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
"Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
"Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer",
"Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
"Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
"National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
"Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
"Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
"Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie",
"Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap",
"Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
"Christian Rock ", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
"Synthpop",
"Christmas", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat",
"Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro",
"Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM",
"Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge",
"Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance",
"Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook",
"Audio Theatre", "Neue Deutsche Welle", "Podcast", "Indie Rock", "G-Funk", "Dubstep",
"Garage Rock", "Psybient",
}
// id3v2Header is a type which represents an ID3v2 tag header.
type id3v2Header struct {
Version Format
Unsynchronisation bool
ExtendedHeader bool
Experimental bool
Size uint
}
// readID3v2Header reads the ID3v2 header from the given io.Reader.
// offset it number of bytes of header that was read
func readID3v2Header(r io.Reader) (h *id3v2Header, offset uint, err error) {
offset = 10
b, err := readBytes(r, offset)
if err != nil {
return nil, 0, fmt.Errorf("expected to read 10 bytes (ID3v2Header): %v", err)
}
if string(b[0:3]) != "ID3" {
return nil, 0, fmt.Errorf("expected to read \"ID3\"")
}
b = b[3:]
var vers Format
switch uint(b[0]) {
case 2:
vers = ID3v2_2
case 3:
vers = ID3v2_3
case 4:
vers = ID3v2_4
case 0, 1:
fallthrough
default:
return nil, 0, fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
}
// NB: We ignore b[1] (the revision) as we don't currently rely on it.
h = &id3v2Header{
Version: vers,
Unsynchronisation: getBit(b[2], 7),
ExtendedHeader: getBit(b[2], 6),
Experimental: getBit(b[2], 5),
Size: uint(get7BitChunkedInt(b[3:7])),
}
if h.ExtendedHeader {
switch vers {
case ID3v2_3:
b, err := readBytes(r, 4)
if err != nil {
return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v23 extended header len): %v", err)
}
// skip header, size is excluding len bytes
extendedHeaderSize := uint(getInt(b))
_, err = readBytes(r, extendedHeaderSize)
if err != nil {
return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v23 skip extended header): %v", extendedHeaderSize, err)
}
offset += extendedHeaderSize
case ID3v2_4:
b, err := readBytes(r, 4)
if err != nil {
return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v24 extended header len): %v", err)
}
// skip header, size is synchsafe int including len bytes
extendedHeaderSize := uint(get7BitChunkedInt(b)) - 4
_, err = readBytes(r, extendedHeaderSize)
if err != nil {
return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v24 skip extended header): %v", extendedHeaderSize, err)
}
offset += extendedHeaderSize
default:
// nop, only 2.3 and 2.4 should have extended header
}
}
return h, offset, nil
}
// id3v2FrameFlags is a type which represents the flags which can be set on an ID3v2 frame.
type id3v2FrameFlags struct {
// Message (ID3 2.3.0 and 2.4.0)
TagAlterPreservation bool
FileAlterPreservation bool
ReadOnly bool
// Format (ID3 2.3.0 and 2.4.0)
Compression bool
Encryption bool
GroupIdentity bool
// ID3 2.4.0 only (see http://id3.org/id3v2.4.0-structure sec 4.1)
Unsynchronisation bool
DataLengthIndicator bool
}
func readID3v23FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
b, err := readBytes(r, 2)
if err != nil {
return nil, err
}
msg := b[0]
fmt := b[1]
return &id3v2FrameFlags{
TagAlterPreservation: getBit(msg, 7),
FileAlterPreservation: getBit(msg, 6),
ReadOnly: getBit(msg, 5),
Compression: getBit(fmt, 7),
Encryption: getBit(fmt, 6),
GroupIdentity: getBit(fmt, 5),
}, nil
}
func readID3v24FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
b, err := readBytes(r, 2)
if err != nil {
return nil, err
}
msg := b[0]
fmt := b[1]
return &id3v2FrameFlags{
TagAlterPreservation: getBit(msg, 6),
FileAlterPreservation: getBit(msg, 5),
ReadOnly: getBit(msg, 4),
GroupIdentity: getBit(fmt, 6),
Compression: getBit(fmt, 3),
Encryption: getBit(fmt, 2),
Unsynchronisation: getBit(fmt, 1),
DataLengthIndicator: getBit(fmt, 0),
}, nil
}
func readID3v2_2FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
name, err = readString(r, 3)
if err != nil {
return
}
size, err = readUint(r, 3)
if err != nil {
return
}
headerSize = 6
return
}
func readID3v2_3FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
name, err = readString(r, 4)
if err != nil {
return
}
size, err = readUint(r, 4)
if err != nil {
return
}
headerSize = 8
return
}
func readID3v2_4FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
name, err = readString(r, 4)
if err != nil {
return
}
size, err = read7BitChunkedUint(r, 4)
if err != nil {
return
}
headerSize = 8
return
}
// readID3v2Frames reads ID3v2 frames from the given reader using the ID3v2Header.
func readID3v2Frames(r io.Reader, offset uint, h *id3v2Header) (map[string]interface{}, error) {
result := make(map[string]interface{})
for offset < h.Size {
var err error
var name string
var size, headerSize uint
var flags *id3v2FrameFlags
switch h.Version {
case ID3v2_2:
name, size, headerSize, err = readID3v2_2FrameHeader(r)
case ID3v2_3:
name, size, headerSize, err = readID3v2_3FrameHeader(r)
if err != nil {
return nil, err
}
flags, err = readID3v23FrameFlags(r)
headerSize += 2
case ID3v2_4:
name, size, headerSize, err = readID3v2_4FrameHeader(r)
if err != nil {
return nil, err
}
flags, err = readID3v24FrameFlags(r)
headerSize += 2
}
if err != nil {
return nil, err
}
// FIXME: Do we still need this?
// if size=0, we certainly are in a padding zone. ignore the rest of
// the tags
if size == 0 {
break
}
offset += headerSize + size
// Avoid corrupted padding (see http://id3.org/Compliance%20Issues).
if !validID3Frame(h.Version, name) && offset > h.Size {
break
}
if flags != nil {
if flags.Compression {
switch h.Version {
case ID3v2_3:
// No data length indicator defined.
if _, err := read7BitChunkedUint(r, 4); err != nil { // read 4
return nil, err
}
size -= 4
case ID3v2_4:
// Must have a data length indicator (to give the size) if compression is enabled.
if !flags.DataLengthIndicator {
return nil, errors.New("compression without data length indicator")
}
default:
return nil, fmt.Errorf("unsupported compression flag used in %v", h.Version)
}
}
if flags.DataLengthIndicator {
if h.Version == ID3v2_3 {
return nil, fmt.Errorf("data length indicator set but not defined for %v", ID3v2_3)
}
size, err = read7BitChunkedUint(r, 4)
if err != nil { // read 4
return nil, err
}
}
if flags.Encryption {
_, err = readBytes(r, 1) // read 1 byte of encryption method
if err != nil {
return nil, err
}
size--
}
}
b, err := readBytes(r, size)
if err != nil {
return nil, err
}
// There can be multiple tag with the same name. Append a number to the
// name if there is more than one.
rawName := name
if _, ok := result[rawName]; ok {
for i := 0; ok; i++ {
rawName = name + "_" + strconv.Itoa(i)
_, ok = result[rawName]
}
}
switch {
case name == "TXXX" || name == "TXX":
t, err := readTextWithDescrFrame(b, false, true) // no lang, but enc
if err != nil {
return nil, err
}
result[rawName] = t
case name[0] == 'T':
txt, err := readTFrame(b)
if err != nil {
return nil, err
}
result[rawName] = txt
case name == "UFID" || name == "UFI":
t, err := readUFID(b)
if err != nil {
return nil, err
}
result[rawName] = t
case name == "WXXX" || name == "WXX":
t, err := readTextWithDescrFrame(b, false, false) // no lang, no enc
if err != nil {
return nil, err
}
result[rawName] = t
case name[0] == 'W':
txt, err := readWFrame(b)
if err != nil {
return nil, err
}
result[rawName] = txt
case name == "COMM" || name == "COM" || name == "USLT" || name == "ULT":
t, err := readTextWithDescrFrame(b, true, true) // both lang and enc
if err != nil {
return nil, fmt.Errorf("could not read %q (%q): %v", name, rawName, err)
}
result[rawName] = t
case name == "APIC":
p, err := readAPICFrame(b)
if err != nil {
return nil, err
}
result[rawName] = p
case name == "PIC":
p, err := readPICFrame(b)
if err != nil {
return nil, err
}
result[rawName] = p
default:
result[rawName] = b
}
}
return result, nil
}
type unsynchroniser struct {
io.Reader
ff bool
}
// filter io.Reader which skip the Unsynchronisation bytes
func (r *unsynchroniser) Read(p []byte) (int, error) {
b := make([]byte, 1)
i := 0
for i < len(p) {
if n, err := r.Reader.Read(b); err != nil || n == 0 {
return i, err
}
if r.ff && b[0] == 0x00 {
r.ff = false
continue
}
p[i] = b[0]
i++
r.ff = (b[0] == 0xFF)
}
return i, nil
}
// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning
// non-nil error on failure.
func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
h, offset, err := readID3v2Header(r)
if err != nil {
return nil, err
}
var ur io.Reader = r
if h.Unsynchronisation {
ur = &unsynchroniser{Reader: r}
}
f, err := readID3v2Frames(ur, offset, h)
if err != nil {
return nil, err
}
return metadataID3v2{header: h, frames: f}, nil
}
var id3v2genreRe = regexp.MustCompile(`(.*[^(]|.* |^)\(([0-9]+)\) *(.*)$`)
// id3v2genre parse a id3v2 genre tag and expand the numeric genres
func id3v2genre(genre string) string {
c := true
for c {
orig := genre
if match := id3v2genreRe.FindStringSubmatch(genre); len(match) > 0 {
if genreID, err := strconv.Atoi(match[2]); err == nil {
if genreID < len(id3v2Genres) {
genre = id3v2Genres[genreID]
if match[1] != "" {
genre = strings.TrimSpace(match[1]) + " " + genre
}
if match[3] != "" {
genre = genre + " " + match[3]
}
}
}
}
c = (orig != genre)
}
return strings.Replace(genre, "((", "(", -1)
}

655
vendor/github.com/dhowden/tag/id3v2frames.go generated vendored Normal file
View File

@ -0,0 +1,655 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"strings"
"unicode/utf16"
)
// DefaultUTF16WithBOMByteOrder is the byte order used when the "UTF16 with BOM" encoding
// is specified without a corresponding BOM in the data.
var DefaultUTF16WithBOMByteOrder binary.ByteOrder = binary.LittleEndian
// ID3v2.2.0 frames (see http://id3.org/id3v2-00, sec 4).
var id3v22Frames = map[string]string{
"BUF": "Recommended buffer size",
"CNT": "Play counter",
"COM": "Comments",
"CRA": "Audio encryption",
"CRM": "Encrypted meta frame",
"ETC": "Event timing codes",
"EQU": "Equalization",
"GEO": "General encapsulated object",
"IPL": "Involved people list",
"LNK": "Linked information",
"MCI": "Music CD Identifier",
"MLL": "MPEG location lookup table",
"PIC": "Attached picture",
"POP": "Popularimeter",
"REV": "Reverb",
"RVA": "Relative volume adjustment",
"SLT": "Synchronized lyric/text",
"STC": "Synced tempo codes",
"TAL": "Album/Movie/Show title",
"TBP": "BPM (Beats Per Minute)",
"TCM": "Composer",
"TCO": "Content type",
"TCR": "Copyright message",
"TDA": "Date",
"TDY": "Playlist delay",
"TEN": "Encoded by",
"TFT": "File type",
"TIM": "Time",
"TKE": "Initial key",
"TLA": "Language(s)",
"TLE": "Length",
"TMT": "Media type",
"TOA": "Original artist(s)/performer(s)",
"TOF": "Original filename",
"TOL": "Original Lyricist(s)/text writer(s)",
"TOR": "Original release year",
"TOT": "Original album/Movie/Show title",
"TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
"TP2": "Band/Orchestra/Accompaniment",
"TP3": "Conductor/Performer refinement",
"TP4": "Interpreted, remixed, or otherwise modified by",
"TPA": "Part of a set",
"TPB": "Publisher",
"TRC": "ISRC (International Standard Recording Code)",
"TRD": "Recording dates",
"TRK": "Track number/Position in set",
"TSI": "Size",
"TSS": "Software/hardware and settings used for encoding",
"TT1": "Content group description",
"TT2": "Title/Songname/Content description",
"TT3": "Subtitle/Description refinement",
"TXT": "Lyricist/text writer",
"TXX": "User defined text information frame",
"TYE": "Year",
"UFI": "Unique file identifier",
"ULT": "Unsychronized lyric/text transcription",
"WAF": "Official audio file webpage",
"WAR": "Official artist/performer webpage",
"WAS": "Official audio source webpage",
"WCM": "Commercial information",
"WCP": "Copyright/Legal information",
"WPB": "Publishers official webpage",
"WXX": "User defined URL link frame",
}
// ID3v2.3.0 frames (see http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
var id3v23Frames = map[string]string{
"AENC": "Audio encryption]",
"APIC": "Attached picture",
"COMM": "Comments",
"COMR": "Commercial frame",
"ENCR": "Encryption method registration",
"EQUA": "Equalization",
"ETCO": "Event timing codes",
"GEOB": "General encapsulated object",
"GRID": "Group identification registration",
"IPLS": "Involved people list",
"LINK": "Linked information",
"MCDI": "Music CD identifier",
"MLLT": "MPEG location lookup table",
"OWNE": "Ownership frame",
"PRIV": "Private frame",
"PCNT": "Play counter",
"POPM": "Popularimeter",
"POSS": "Position synchronisation frame",
"RBUF": "Recommended buffer size",
"RVAD": "Relative volume adjustment",
"RVRB": "Reverb",
"SYLT": "Synchronized lyric/text",
"SYTC": "Synchronized tempo codes",
"TALB": "Album/Movie/Show title",
"TBPM": "BPM (beats per minute)",
"TCMP": "iTunes Compilation Flag",
"TCOM": "Composer",
"TCON": "Content type",
"TCOP": "Copyright message",
"TDAT": "Date",
"TDLY": "Playlist delay",
"TENC": "Encoded by",
"TEXT": "Lyricist/Text writer",
"TFLT": "File type",
"TIME": "Time",
"TIT1": "Content group description",
"TIT2": "Title/songname/content description",
"TIT3": "Subtitle/Description refinement",
"TKEY": "Initial key",
"TLAN": "Language(s)",
"TLEN": "Length",
"TMED": "Media type",
"TOAL": "Original album/movie/show title",
"TOFN": "Original filename",
"TOLY": "Original lyricist(s)/text writer(s)",
"TOPE": "Original artist(s)/performer(s)",
"TORY": "Original release year",
"TOWN": "File owner/licensee",
"TPE1": "Lead performer(s)/Soloist(s)",
"TPE2": "Band/orchestra/accompaniment",
"TPE3": "Conductor/performer refinement",
"TPE4": "Interpreted, remixed, or otherwise modified by",
"TPOS": "Part of a set",
"TPUB": "Publisher",
"TRCK": "Track number/Position in set",
"TRDA": "Recording dates",
"TRSN": "Internet radio station name",
"TRSO": "Internet radio station owner",
"TSIZ": "Size",
"TSO2": "iTunes uses this for Album Artist sort order",
"TSOC": "iTunes uses this for Composer sort order",
"TSRC": "ISRC (international standard recording code)",
"TSSE": "Software/Hardware and settings used for encoding",
"TYER": "Year",
"TXXX": "User defined text information frame",
"UFID": "Unique file identifier",
"USER": "Terms of use",
"USLT": "Unsychronized lyric/text transcription",
"WCOM": "Commercial information",
"WCOP": "Copyright/Legal information",
"WOAF": "Official audio file webpage",
"WOAR": "Official artist/performer webpage",
"WOAS": "Official audio source webpage",
"WORS": "Official internet radio station homepage",
"WPAY": "Payment",
"WPUB": "Publishers official webpage",
"WXXX": "User defined URL link frame",
}
// ID3v2.4.0 frames (see http://id3.org/id3v2.4.0-frames, sec 4).
var id3v24Frames = map[string]string{
"AENC": "Audio encryption",
"APIC": "Attached picture",
"ASPI": "Audio seek point index",
"COMM": "Comments",
"COMR": "Commercial frame",
"ENCR": "Encryption method registration",
"EQU2": "Equalisation (2)",
"ETCO": "Event timing codes",
"GEOB": "General encapsulated object",
"GRID": "Group identification registration",
"LINK": "Linked information",
"MCDI": "Music CD identifier",
"MLLT": "MPEG location lookup table",
"OWNE": "Ownership frame",
"PRIV": "Private frame",
"PCNT": "Play counter",
"POPM": "Popularimeter",
"POSS": "Position synchronisation frame",
"RBUF": "Recommended buffer size",
"RVA2": "Relative volume adjustment (2)",
"RVRB": "Reverb",
"SEEK": "Seek frame",
"SIGN": "Signature frame",
"SYLT": "Synchronised lyric/text",
"SYTC": "Synchronised tempo codes",
"TALB": "Album/Movie/Show title",
"TBPM": "BPM (beats per minute)",
"TCMP": "iTunes Compilation Flag",
"TCOM": "Composer",
"TCON": "Content type",
"TCOP": "Copyright message",
"TDEN": "Encoding time",
"TDLY": "Playlist delay",
"TDOR": "Original release time",
"TDRC": "Recording time",
"TDRL": "Release time",
"TDTG": "Tagging time",
"TENC": "Encoded by",
"TEXT": "Lyricist/Text writer",
"TFLT": "File type",
"TIPL": "Involved people list",
"TIT1": "Content group description",
"TIT2": "Title/songname/content description",
"TIT3": "Subtitle/Description refinement",
"TKEY": "Initial key",
"TLAN": "Language(s)",
"TLEN": "Length",
"TMCL": "Musician credits list",
"TMED": "Media type",
"TMOO": "Mood",
"TOAL": "Original album/movie/show title",
"TOFN": "Original filename",
"TOLY": "Original lyricist(s)/text writer(s)",
"TOPE": "Original artist(s)/performer(s)",
"TOWN": "File owner/licensee",
"TPE1": "Lead performer(s)/Soloist(s)",
"TPE2": "Band/orchestra/accompaniment",
"TPE3": "Conductor/performer refinement",
"TPE4": "Interpreted, remixed, or otherwise modified by",
"TPOS": "Part of a set",
"TPRO": "Produced notice",
"TPUB": "Publisher",
"TRCK": "Track number/Position in set",
"TRSN": "Internet radio station name",
"TRSO": "Internet radio station owner",
"TSO2": "iTunes uses this for Album Artist sort order",
"TSOA": "Album sort order",
"TSOC": "iTunes uses this for Composer sort order",
"TSOP": "Performer sort order",
"TSOT": "Title sort order",
"TSRC": "ISRC (international standard recording code)",
"TSSE": "Software/Hardware and settings used for encoding",
"TSST": "Set subtitle",
"TXXX": "User defined text information frame",
"UFID": "Unique file identifier",
"USER": "Terms of use",
"USLT": "Unsynchronised lyric/text transcription",
"WCOM": "Commercial information",
"WCOP": "Copyright/Legal information",
"WOAF": "Official audio file webpage",
"WOAR": "Official artist/performer webpage",
"WOAS": "Official audio source webpage",
"WORS": "Official Internet radio station homepage",
"WPAY": "Payment",
"WPUB": "Publishers official webpage",
"WXXX": "User defined URL link frame",
}
// ID3 frames that are defined in the specs.
var id3Frames = map[Format]map[string]string{
ID3v2_2: id3v22Frames,
ID3v2_3: id3v23Frames,
ID3v2_4: id3v24Frames,
}
func validID3Frame(version Format, name string) bool {
names, ok := id3Frames[version]
if !ok {
return false
}
_, ok = names[name]
return ok
}
func readWFrame(b []byte) (string, error) {
// Frame text is always encoded in ISO-8859-1
b = append([]byte{0}, b...)
return readTFrame(b)
}
func readTFrame(b []byte) (string, error) {
if len(b) == 0 {
return "", nil
}
txt, err := decodeText(b[0], b[1:])
if err != nil {
return "", err
}
return strings.Join(strings.Split(txt, string(singleZero)), ""), nil
}
const (
encodingISO8859 byte = 0
encodingUTF16WithBOM byte = 1
encodingUTF16 byte = 2
encodingUTF8 byte = 3
)
func decodeText(enc byte, b []byte) (string, error) {
if len(b) == 0 {
return "", nil
}
switch enc {
case encodingISO8859: // ISO-8859-1
return decodeISO8859(b), nil
case encodingUTF16WithBOM: // UTF-16 with byte order marker
if len(b) == 1 {
return "", nil
}
return decodeUTF16WithBOM(b)
case encodingUTF16: // UTF-16 without byte order (assuming BigEndian)
if len(b) == 1 {
return "", nil
}
return decodeUTF16(b, binary.BigEndian)
case encodingUTF8: // UTF-8
return string(b), nil
default: // Fallback to ISO-8859-1
return decodeISO8859(b), nil
}
}
var (
singleZero = []byte{0}
doubleZero = []byte{0, 0}
)
func dataSplit(b []byte, enc byte) [][]byte {
delim := singleZero
if enc == encodingUTF16 || enc == encodingUTF16WithBOM {
delim = doubleZero
}
result := bytes.SplitN(b, delim, 2)
if len(result) != 2 {
return result
}
if len(result[1]) == 0 {
return result
}
if result[1][0] == 0 {
// there was a double (or triple) 0 and we cut too early
result[0] = append(result[0], result[1][0])
result[1] = result[1][1:]
}
return result
}
func decodeISO8859(b []byte) string {
r := make([]rune, len(b))
for i, x := range b {
r[i] = rune(x)
}
return string(r)
}
func decodeUTF16WithBOM(b []byte) (string, error) {
if len(b) < 2 {
return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark")
}
var bo binary.ByteOrder
switch {
case b[0] == 0xFE && b[1] == 0xFF:
bo = binary.BigEndian
b = b[2:]
case b[0] == 0xFF && b[1] == 0xFE:
bo = binary.LittleEndian
b = b[2:]
default:
bo = DefaultUTF16WithBOMByteOrder
}
return decodeUTF16(b, bo)
}
func decodeUTF16(b []byte, bo binary.ByteOrder) (string, error) {
if len(b)%2 != 0 {
return "", errors.New("invalid encoding: expected even number of bytes for UTF-16 encoded text")
}
s := make([]uint16, 0, len(b)/2)
for i := 0; i < len(b); i += 2 {
s = append(s, bo.Uint16(b[i:i+2]))
}
return string(utf16.Decode(s)), nil
}
// Comm is a type used in COMM, UFID, TXXX, WXXX and USLT tag.
// It's a text with a description and a specified language
// For WXXX, TXXX and UFID, we don't set a Language
type Comm struct {
Language string
Description string
Text string
}
// String returns a string representation of the underlying Comm instance.
func (t Comm) String() string {
if t.Language != "" {
return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}",
t.Language, t.Description, strings.Count(t.Text, "\n"))
}
return fmt.Sprintf("Text{Description: '%v', %v}", t.Description, t.Text)
}
// IDv2.{3,4}
// -- Header
// <Header for 'Unsynchronised lyrics/text transcription', ID: "USLT">
// <Header for 'Comment', ID: "COMM">
// -- readTextWithDescrFrame(data, true, true)
// Text encoding $xx
// Language $xx xx xx
// Content descriptor <text string according to encoding> $00 (00)
// Lyrics/text <full text string according to encoding>
// -- Header
// <Header for 'User defined text information frame', ID: "TXXX">
// <Header for 'User defined URL link frame', ID: "WXXX">
// -- readTextWithDescrFrame(data, false, <isDataEncoded>)
// Text encoding $xx
// Description <text string according to encoding> $00 (00)
// Value <text string according to encoding>
func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) {
if len(b) == 0 {
return nil, errors.New("error decoding tag description text: invalid encoding")
}
enc := b[0]
b = b[1:]
c := &Comm{}
if hasLang {
if len(b) < 3 {
return nil, errors.New("hasLang set but not enough data for language information")
}
c.Language = string(b[:3])
b = b[3:]
}
descTextSplit := dataSplit(b, enc)
if len(descTextSplit) == 0 {
return nil, errors.New("error decoding tag description text: invalid encoding")
}
desc, err := decodeText(enc, descTextSplit[0])
if err != nil {
return nil, fmt.Errorf("error decoding tag description text: %v", err)
}
c.Description = desc
if len(descTextSplit) == 1 {
return c, nil
}
if !encoded {
enc = byte(0)
}
text, err := decodeText(enc, descTextSplit[1])
if err != nil {
return nil, fmt.Errorf("error decoding tag text: %v", err)
}
c.Text = text
return c, nil
}
// UFID is composed of a provider (frequently a URL and a binary identifier)
// The identifier can be a text (Musicbrainz use texts, but not necessary)
type UFID struct {
Provider string
Identifier []byte
}
func (u UFID) String() string {
return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier))
}
func readUFID(b []byte) (*UFID, error) {
result := bytes.SplitN(b, singleZero, 2)
if len(result) != 2 {
return nil, errors.New("expected to split UFID data into 2 pieces")
}
return &UFID{
Provider: string(result[0]),
Identifier: result[1],
}, nil
}
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",
}
// Picture is a type which represents an attached picture extracted from metadata.
type Picture struct {
Ext string // Extension of the picture file.
MIMEType string // MIMEType of the picture.
Type string // Type of the picture (see pictureTypes).
Description string // Description.
Data []byte // Raw picture data.
}
// String returns a string representation of the underlying Picture instance.
func (p Picture) String() string {
return fmt.Sprintf("Picture{Ext: %v, MIMEType: %v, Type: %v, Description: %v, Data.Size: %v}",
p.Ext, p.MIMEType, p.Type, p.Description, len(p.Data))
}
// IDv2.2
// -- Header
// Attached picture "PIC"
// Frame size $xx xx xx
// -- readPICFrame
// Text encoding $xx
// Image format $xx xx xx
// Picture type $xx
// Description <textstring> $00 (00)
// Picture data <binary data>
func readPICFrame(b []byte) (*Picture, error) {
if len(b) < 5 {
return nil, errors.New("invalid PIC frame")
}
enc := b[0]
ext := string(b[1:4])
picType := b[4]
descDataSplit := dataSplit(b[5:], enc)
if len(descDataSplit) != 2 {
return nil, errors.New("error decoding PIC description text: invalid encoding")
}
desc, err := decodeText(enc, descDataSplit[0])
if err != nil {
return nil, fmt.Errorf("error decoding PIC description text: %v", err)
}
var mimeType string
switch ext {
case "jpeg", "jpg":
mimeType = "image/jpeg"
case "png":
mimeType = "image/png"
}
return &Picture{
Ext: ext,
MIMEType: mimeType,
Type: pictureTypes[picType],
Description: desc,
Data: descDataSplit[1],
}, nil
}
// IDv2.{3,4}
// -- Header
// <Header for 'Attached picture', ID: "APIC">
// -- readAPICFrame
// Text encoding $xx
// MIME type <text string> $00
// Picture type $xx
// Description <text string according to encoding> $00 (00)
// Picture data <binary data>
func readAPICFrame(b []byte) (*Picture, error) {
if len(b) == 0 {
return nil, errors.New("error decoding APIC: invalid encoding")
}
enc := b[0]
mimeDataSplit := bytes.SplitN(b[1:], singleZero, 2)
if len(mimeDataSplit) != 2 {
return nil, errors.New("error decoding APIC: invalid encoding")
}
mimeType := string(mimeDataSplit[0])
b = mimeDataSplit[1]
if len(b) < 1 {
return nil, fmt.Errorf("error decoding APIC mimetype")
}
picType := b[0]
descDataSplit := dataSplit(b[1:], enc)
if len(descDataSplit) != 2 {
return nil, errors.New("error decoding APIC description text: invalid encoding")
}
desc, err := decodeText(enc, descDataSplit[0])
if err != nil {
return nil, fmt.Errorf("error decoding APIC description text: %v", err)
}
var ext string
switch mimeType {
case "image/jpeg":
ext = "jpg"
case "image/png":
ext = "png"
}
return &Picture{
Ext: ext,
MIMEType: mimeType,
Type: pictureTypes[picType],
Description: desc,
Data: descDataSplit[1],
}, nil
}

152
vendor/github.com/dhowden/tag/id3v2metadata.go generated vendored Normal file
View File

@ -0,0 +1,152 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"strconv"
"strings"
"time"
)
type frameNames map[string][2]string
func (f frameNames) Name(s string, fm Format) string {
l, ok := f[s]
if !ok {
return ""
}
switch fm {
case ID3v2_2:
return l[0]
case ID3v2_3:
return l[1]
case ID3v2_4:
if s == "year" {
return "TDRC"
}
return l[1]
}
return ""
}
var frames = frameNames(map[string][2]string{
"title": [2]string{"TT2", "TIT2"},
"artist": [2]string{"TP1", "TPE1"},
"album": [2]string{"TAL", "TALB"},
"album_artist": [2]string{"TP2", "TPE2"},
"composer": [2]string{"TCM", "TCOM"},
"year": [2]string{"TYE", "TYER"},
"track": [2]string{"TRK", "TRCK"},
"disc": [2]string{"TPA", "TPOS"},
"genre": [2]string{"TCO", "TCON"},
"picture": [2]string{"PIC", "APIC"},
"lyrics": [2]string{"", "USLT"},
"comment": [2]string{"COM", "COMM"},
})
// metadataID3v2 is the implementation of Metadata used for ID3v2 tags.
type metadataID3v2 struct {
header *id3v2Header
frames map[string]interface{}
}
func (m metadataID3v2) getString(k string) string {
v, ok := m.frames[k]
if !ok {
return ""
}
return v.(string)
}
func (m metadataID3v2) Format() Format { return m.header.Version }
func (m metadataID3v2) FileType() FileType { return MP3 }
func (m metadataID3v2) Raw() map[string]interface{} { return m.frames }
func (m metadataID3v2) Title() string {
return m.getString(frames.Name("title", m.Format()))
}
func (m metadataID3v2) Artist() string {
return m.getString(frames.Name("artist", m.Format()))
}
func (m metadataID3v2) Album() string {
return m.getString(frames.Name("album", m.Format()))
}
func (m metadataID3v2) AlbumArtist() string {
return m.getString(frames.Name("album_artist", m.Format()))
}
func (m metadataID3v2) Composer() string {
return m.getString(frames.Name("composer", m.Format()))
}
func (m metadataID3v2) Genre() string {
return id3v2genre(m.getString(frames.Name("genre", m.Format())))
}
func (m metadataID3v2) Year() int {
stringYear := m.getString(frames.Name("year", m.Format()))
if year, err := strconv.Atoi(stringYear); err == nil {
return year
}
date, err := time.Parse(time.DateOnly, stringYear)
if err != nil {
return 0
}
return date.Year()
}
func parseXofN(s string) (x, n int) {
xn := strings.Split(s, "/")
if len(xn) != 2 {
x, _ = strconv.Atoi(s)
return x, 0
}
x, _ = strconv.Atoi(strings.TrimSpace(xn[0]))
n, _ = strconv.Atoi(strings.TrimSpace(xn[1]))
return x, n
}
func (m metadataID3v2) Track() (int, int) {
return parseXofN(m.getString(frames.Name("track", m.Format())))
}
func (m metadataID3v2) Disc() (int, int) {
return parseXofN(m.getString(frames.Name("disc", m.Format())))
}
func (m metadataID3v2) Lyrics() string {
t, ok := m.frames[frames.Name("lyrics", m.Format())]
if !ok {
return ""
}
return t.(*Comm).Text
}
func (m metadataID3v2) Comment() string {
t, ok := m.frames[frames.Name("comment", m.Format())]
if !ok {
return ""
}
// id3v23 has Text, id3v24 has Description
if t.(*Comm).Description == "" {
return trimString(t.(*Comm).Text)
}
return trimString(t.(*Comm).Description)
}
func (m metadataID3v2) Picture() *Picture {
v, ok := m.frames[frames.Name("picture", m.Format())]
if !ok {
return nil
}
return v.(*Picture)
}

379
vendor/github.com/dhowden/tag/mp4.go generated vendored Normal file
View File

@ -0,0 +1,379 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
var atomTypes = map[int]string{
0: "implicit", // automatic based on atom name
1: "text",
13: "jpeg",
14: "png",
21: "uint8",
}
// NB: atoms does not include "----", this is handled separately
var atoms = atomNames(map[string]string{
"\xa9alb": "album",
"\xa9art": "artist",
"\xa9ART": "artist",
"aART": "album_artist",
"\xa9day": "year",
"\xa9nam": "title",
"\xa9gen": "genre",
"trkn": "track",
"\xa9wrt": "composer",
"\xa9too": "encoder",
"cprt": "copyright",
"covr": "picture",
"\xa9grp": "grouping",
"keyw": "keyword",
"\xa9lyr": "lyrics",
"\xa9cmt": "comment",
"tmpo": "tempo",
"cpil": "compilation",
"disk": "disc",
})
var means = map[string]bool{
"com.apple.iTunes": true,
"com.mixedinkey.mixedinkey": true,
"com.serato.dj": true,
}
// Detect PNG image if "implicit" class is used
var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
type atomNames map[string]string
func (f atomNames) Name(n string) []string {
res := make([]string, 1)
for k, v := range f {
if v == n {
res = append(res, k)
}
}
return res
}
// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data.
type metadataMP4 struct {
fileType FileType
data map[string]interface{}
}
// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning
// non-nil error if there was a problem.
func ReadAtoms(r io.ReadSeeker) (Metadata, error) {
m := metadataMP4{
data: make(map[string]interface{}),
fileType: UnknownFileType,
}
err := m.readAtoms(r)
return m, err
}
func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
for {
name, size, err := readAtomHeader(r)
if err != nil {
if err == io.EOF {
return nil
}
return err
}
switch name {
case "meta":
// next_item_id (int32)
_, err := readBytes(r, 4)
if err != nil {
return err
}
fallthrough
case "moov", "udta", "ilst":
return m.readAtoms(r)
}
_, ok := atoms[name]
var data []string
if name == "----" {
name, data, err = readCustomAtom(r, size)
if err != nil {
return err
}
if name != "----" {
ok = true
size = 0 // already read data
}
}
if !ok {
_, err := r.Seek(int64(size-8), io.SeekCurrent)
if err != nil {
return err
}
continue
}
err = m.readAtomData(r, name, size-8, data)
if err != nil {
return err
}
}
}
func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error {
var b []byte
var err error
var contentType string
if len(processedData) > 0 {
b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields
contentType = "text"
} else {
// read the data
b, err = readBytes(r, uint(size))
if err != nil {
return err
}
if len(b) < 8 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 8, len(b))
}
// "data" + size (4 bytes each)
b = b[8:]
if len(b) < 4 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 4, len(b))
}
class := getInt(b[1:4])
var ok bool
contentType, ok = atomTypes[class]
if !ok {
return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b)
}
// 4: atom version (1 byte) + atom flags (3 bytes)
// 4: NULL (usually locale indicator)
if len(b) < 8 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for atom version and flags, got %d", 8, len(b))
}
b = b[8:]
}
if name == "trkn" || name == "disk" {
if len(b) < 6 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for track and disk numbers, got %d", 6, len(b))
}
m.data[name] = int(b[3])
m.data[name+"_count"] = int(b[5])
return nil
}
if contentType == "implicit" {
if name == "covr" {
if bytes.HasPrefix(b, pngHeader) {
contentType = "png"
}
// TODO(dhowden): Detect JPEG formats too (harder).
}
}
var data interface{}
switch contentType {
case "implicit":
if _, ok := atoms[name]; ok {
return fmt.Errorf("unhandled implicit content type for required atom: %q", name)
}
return nil
case "text":
data = string(b)
case "uint8":
if len(b) < 1 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b))
}
data = getInt(b[:1])
case "jpeg", "png":
data = &Picture{
Ext: contentType,
MIMEType: "image/" + contentType,
Data: b,
}
}
m.data[name] = data
return nil
}
func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
err = binary.Read(r, binary.BigEndian, &size)
if err != nil {
return
}
name, err = readString(r, 4)
return
}
// Generic atom.
// Should have 3 sub atoms : mean, name and data.
// We check that mean is "com.apple.iTunes" or others and we use the subname as
// the name, and move to the data atom.
// Data atom could have multiple data values, each with a header.
// If anything goes wrong, we jump at the end of the "----" atom.
func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ error) {
subNames := make(map[string]string)
for size > 8 {
subName, subSize, err := readAtomHeader(r)
if err != nil {
return "", nil, err
}
// Remove the size of the atom from the size counter
if size >= subSize {
size -= subSize
} else {
return "", nil, errors.New("--- invalid size")
}
b, err := readBytes(r, uint(subSize-8))
if err != nil {
return "", nil, err
}
if len(b) < 4 {
return "", nil, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b))
}
switch subName {
case "mean", "name":
subNames[subName] = string(b[4:])
case "data":
data = append(data, string(b[4:]))
}
}
// there should remain only the header size
if size != 8 {
err := errors.New("---- atom out of bounds")
return "", nil, err
}
if !means[subNames["mean"]] || subNames["name"] == "" || len(data) == 0 {
return "----", nil, nil
}
return subNames["name"], data, nil
}
func (metadataMP4) Format() Format { return MP4 }
func (m metadataMP4) FileType() FileType { return m.fileType }
func (m metadataMP4) Raw() map[string]interface{} { return m.data }
func (m metadataMP4) getString(n []string) string {
for _, k := range n {
if x, ok := m.data[k]; ok {
return x.(string)
}
}
return ""
}
func (m metadataMP4) getInt(n []string) int {
for _, k := range n {
if x, ok := m.data[k]; ok {
return x.(int)
}
}
return 0
}
func (m metadataMP4) Title() string {
return m.getString(atoms.Name("title"))
}
func (m metadataMP4) Artist() string {
return m.getString(atoms.Name("artist"))
}
func (m metadataMP4) Album() string {
return m.getString(atoms.Name("album"))
}
func (m metadataMP4) AlbumArtist() string {
return m.getString(atoms.Name("album_artist"))
}
func (m metadataMP4) Composer() string {
return m.getString(atoms.Name("composer"))
}
func (m metadataMP4) Genre() string {
return m.getString(atoms.Name("genre"))
}
func (m metadataMP4) Year() int {
date := m.getString(atoms.Name("year"))
if len(date) >= 4 {
year, _ := strconv.Atoi(date[:4])
return year
}
return 0
}
func (m metadataMP4) Track() (int, int) {
x := m.getInt([]string{"trkn"})
if n, ok := m.data["trkn_count"]; ok {
return x, n.(int)
}
return x, 0
}
func (m metadataMP4) Disc() (int, int) {
x := m.getInt([]string{"disk"})
if n, ok := m.data["disk_count"]; ok {
return x, n.(int)
}
return x, 0
}
func (m metadataMP4) Lyrics() string {
t, ok := m.data["\xa9lyr"]
if !ok {
return ""
}
return t.(string)
}
func (m metadataMP4) Comment() string {
t, ok := m.data["\xa9cmt"]
if !ok {
return ""
}
return t.(string)
}
func (m metadataMP4) Picture() *Picture {
v, ok := m.data["covr"]
if !ok {
return nil
}
p, _ := v.(*Picture)
return p
}

174
vendor/github.com/dhowden/tag/ogg.go generated vendored Normal file
View File

@ -0,0 +1,174 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
)
var (
vorbisCommentPrefix = []byte("\x03vorbis")
opusTagsPrefix = []byte("OpusTags")
)
var oggCRC32Poly04c11db7 = oggCRCTable(0x04c11db7)
type crc32Table [256]uint32
func oggCRCTable(poly uint32) *crc32Table {
var t crc32Table
for i := 0; i < 256; i++ {
crc := uint32(i) << 24
for j := 0; j < 8; j++ {
if crc&0x80000000 != 0 {
crc = (crc << 1) ^ poly
} else {
crc <<= 1
}
}
t[i] = crc
}
return &t
}
func oggCRCUpdate(crc uint32, tab *crc32Table, p []byte) uint32 {
for _, v := range p {
crc = (crc << 8) ^ tab[byte(crc>>24)^v]
}
return crc
}
type oggPageHeader struct {
Magic [4]byte // "OggS"
Version uint8
Flags uint8
GranulePosition uint64
SerialNumber uint32
SequenceNumber uint32
CRC uint32
Segments uint8
}
type oggDemuxer struct {
packetBufs map[uint32]*bytes.Buffer
}
// Read ogg packets, can return empty slice of packets and nil err
// if more data is needed
func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) {
headerBuf := &bytes.Buffer{}
var oh oggPageHeader
if err := binary.Read(io.TeeReader(r, headerBuf), binary.LittleEndian, &oh); err != nil {
return nil, err
}
if bytes.Compare(oh.Magic[:], []byte("OggS")) != 0 {
// TODO: seek for syncword?
return nil, errors.New("expected 'OggS'")
}
segmentTable := make([]byte, oh.Segments)
if _, err := io.ReadFull(r, segmentTable); err != nil {
return nil, err
}
var segmentsSize int64
for _, s := range segmentTable {
segmentsSize += int64(s)
}
segmentsData := make([]byte, segmentsSize)
if _, err := io.ReadFull(r, segmentsData); err != nil {
return nil, err
}
headerBytes := headerBuf.Bytes()
// reset CRC to zero in header before checksum
headerBytes[22] = 0
headerBytes[23] = 0
headerBytes[24] = 0
headerBytes[25] = 0
crc := oggCRCUpdate(0, oggCRC32Poly04c11db7, headerBytes)
crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentTable)
crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentsData)
if crc != oh.CRC {
return nil, fmt.Errorf("expected crc %x != %x", oh.CRC, crc)
}
if o.packetBufs == nil {
o.packetBufs = map[uint32]*bytes.Buffer{}
}
var packetBuf *bytes.Buffer
continued := oh.Flags&0x1 != 0
if continued {
if b, ok := o.packetBufs[oh.SerialNumber]; ok {
packetBuf = b
} else {
return nil, fmt.Errorf("could not find continued packet %d", oh.SerialNumber)
}
} else {
packetBuf = &bytes.Buffer{}
}
var packets [][]byte
var p int
for _, s := range segmentTable {
packetBuf.Write(segmentsData[p : p+int(s)])
if s < 255 {
packets = append(packets, packetBuf.Bytes())
packetBuf = &bytes.Buffer{}
}
p += int(s)
}
o.packetBufs[oh.SerialNumber] = packetBuf
return packets, nil
}
// ReadOGGTags reads OGG metadata from the io.ReadSeeker, returning the resulting
// metadata in a Metadata implementation, or non-nil error if there was a problem.
// See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html
// and http://www.xiph.org/ogg/doc/framing.html for details.
// For Opus see https://tools.ietf.org/html/rfc7845
func ReadOGGTags(r io.Reader) (Metadata, error) {
od := &oggDemuxer{}
for {
bs, err := od.Read(r)
if err != nil {
return nil, err
}
for _, b := range bs {
switch {
case bytes.HasPrefix(b, vorbisCommentPrefix):
m := &metadataOGG{
newMetadataVorbis(),
}
err = m.readVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):]))
return m, err
case bytes.HasPrefix(b, opusTagsPrefix):
m := &metadataOGG{
newMetadataVorbis(),
}
err = m.readVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):]))
return m, err
}
}
}
}
type metadataOGG struct {
*metadataVorbis
}
func (m *metadataOGG) FileType() FileType {
return OGG
}

219
vendor/github.com/dhowden/tag/sum.go generated vendored Normal file
View File

@ -0,0 +1,219 @@
package tag
import (
"crypto/sha1"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
)
// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
// (ID3, MP4) invariant.
func Sum(r io.ReadSeeker) (string, error) {
b, err := readBytes(r, 11)
if err != nil {
return "", err
}
_, err = r.Seek(-11, io.SeekCurrent)
if err != nil {
return "", fmt.Errorf("could not seek back to original position: %v", err)
}
switch {
case string(b[0:4]) == "fLaC":
return SumFLAC(r)
case string(b[4:11]) == "ftypM4A":
return SumAtoms(r)
case string(b[0:3]) == "ID3":
return SumID3v2(r)
}
h, err := SumID3v1(r)
if err != nil {
if err == ErrNotID3v1 {
return SumAll(r)
}
return "", err
}
return h, nil
}
// SumAll returns a checksum of the content from the reader (until EOF).
func SumAll(r io.ReadSeeker) (string, error) {
h := sha1.New()
_, err := io.Copy(h, r)
if err != nil {
return "", nil
}
return hashSum(h), nil
}
// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is
// metadata invariant.
func SumAtoms(r io.ReadSeeker) (string, error) {
for {
var size uint32
err := binary.Read(r, binary.BigEndian, &size)
if err != nil {
if err == io.EOF {
return "", fmt.Errorf("reached EOF before audio data")
}
return "", err
}
name, err := readString(r, 4)
if err != nil {
return "", err
}
switch name {
case "meta":
// next_item_id (int32)
_, err := r.Seek(4, io.SeekCurrent)
if err != nil {
return "", err
}
fallthrough
case "moov", "udta", "ilst":
continue
case "mdat": // stop when we get to the data
h := sha1.New()
_, err := io.CopyN(h, r, int64(size-8))
if err != nil {
return "", fmt.Errorf("error reading audio data: %v", err)
}
return hashSum(h), nil
}
_, err = r.Seek(int64(size-8), io.SeekCurrent)
if err != nil {
return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
}
}
}
func sizeToEndOffset(r io.ReadSeeker, offset int64) (int64, error) {
n, err := r.Seek(-128, io.SeekEnd)
if err != nil {
return 0, fmt.Errorf("error seeking end offset (%d bytes): %v", offset, err)
}
_, err = r.Seek(-n, io.SeekCurrent)
if err != nil {
return 0, fmt.Errorf("error seeking back to original position: %v", err)
}
return n, nil
}
// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided
// by the io.ReadSeeker which is metadata invariant.
func SumID3v1(r io.ReadSeeker) (string, error) {
n, err := sizeToEndOffset(r, 128)
if err != nil {
return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
}
// TODO: improve this check???
if n <= 0 {
return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3")
}
h := sha1.New()
_, err = io.CopyN(h, r, n)
if err != nil {
return "", fmt.Errorf("error reading %v bytes: %v", n, err)
}
return hashSum(h), nil
}
// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
// io.ReadSeeker which is metadata invariant.
func SumID3v2(r io.ReadSeeker) (string, error) {
header, _, err := readID3v2Header(r)
if err != nil {
return "", fmt.Errorf("error reading ID3v2 header: %v", err)
}
_, err = r.Seek(int64(header.Size), io.SeekCurrent)
if err != nil {
return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
}
n, err := sizeToEndOffset(r, 128)
if err != nil {
return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
}
// TODO: remove this check?????
if n < 0 {
return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n)
}
h := sha1.New()
_, err = io.CopyN(h, r, n)
if err != nil {
return "", fmt.Errorf("error reading %v bytes: %v", n, err)
}
return hashSum(h), nil
}
// SumFLAC costructs a checksum of the FLAC audio file data provided by the io.ReadSeeker (ignores
// metadata fields).
func SumFLAC(r io.ReadSeeker) (string, error) {
flac, err := readString(r, 4)
if err != nil {
return "", err
}
if flac != "fLaC" {
return "", errors.New("expected 'fLaC'")
}
for {
last, err := skipFLACMetadataBlock(r)
if err != nil {
return "", err
}
if last {
break
}
}
h := sha1.New()
_, err = io.Copy(h, r)
if err != nil {
return "", fmt.Errorf("error reading data bytes from FLAC: %v", err)
}
return hashSum(h), nil
}
func skipFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
blockHeader, err := readBytes(r, 1)
if err != nil {
return
}
if getBit(blockHeader[0], 7) {
blockHeader[0] ^= (1 << 7)
last = true
}
blockLen, err := readInt(r, 3)
if err != nil {
return
}
_, err = r.Seek(int64(blockLen), io.SeekCurrent)
return
}
func hashSum(h hash.Hash) string {
return fmt.Sprintf("%x", h.Sum([]byte{}))
}

147
vendor/github.com/dhowden/tag/tag.go generated vendored Normal file
View File

@ -0,0 +1,147 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package tag provides MP3 (ID3: v1, 2.2, 2.3 and 2.4), MP4, FLAC and OGG metadata detection,
// parsing and artwork extraction.
//
// Detect and parse tag metadata from an io.ReadSeeker (i.e. an *os.File):
// m, err := tag.ReadFrom(f)
// if err != nil {
// log.Fatal(err)
// }
// log.Print(m.Format()) // The detected format.
// log.Print(m.Title()) // The title of the track (see Metadata interface for more details).
package tag
import (
"errors"
"fmt"
"io"
)
// ErrNoTagsFound is the error returned by ReadFrom when the metadata format
// cannot be identified.
var ErrNoTagsFound = errors.New("no tags found")
// ReadFrom detects and parses audio file metadata tags (currently supports ID3v1,2.{2,3,4}, MP4, FLAC/OGG).
// Returns non-nil error if the format of the given data could not be determined, or if there was a problem
// parsing the data.
func ReadFrom(r io.ReadSeeker) (Metadata, error) {
b, err := readBytes(r, 11)
if err != nil {
return nil, err
}
_, err = r.Seek(-11, io.SeekCurrent)
if err != nil {
return nil, fmt.Errorf("could not seek back to original position: %v", err)
}
switch {
case string(b[0:4]) == "fLaC":
return ReadFLACTags(r)
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)
case string(b[0:4]) == "DSD ":
return ReadDSFTags(r)
}
m, err := ReadID3v1Tags(r)
if err != nil {
if err == ErrNotID3v1 {
err = ErrNoTagsFound
}
return nil, err
}
return m, nil
}
// Format is an enumeration of metadata types supported by this package.
type Format string
// Supported tag formats.
const (
UnknownFormat Format = "" // Unknown Format.
ID3v1 Format = "ID3v1" // ID3v1 tag format.
ID3v2_2 Format = "ID3v2.2" // ID3v2.2 tag format.
ID3v2_3 Format = "ID3v2.3" // ID3v2.3 tag format (most common).
ID3v2_4 Format = "ID3v2.4" // ID3v2.4 tag format.
MP4 Format = "MP4" // MP4 tag (atom) format (see http://www.ftyps.com/ for a full file type list)
VORBIS Format = "VORBIS" // Vorbis Comment tag format.
)
// FileType is an enumeration of the audio file types supported by this package, in particular
// there are audio file types which share metadata formats, and this type is used to distinguish
// between them.
type FileType string
// Supported file types.
const (
UnknownFileType FileType = "" // Unknown FileType.
MP3 FileType = "MP3" // MP3 file
M4A FileType = "M4A" // M4A file Apple iTunes (ACC) Audio
M4B FileType = "M4B" // M4A file Apple iTunes (ACC) Audio Book
M4P FileType = "M4P" // M4A file Apple iTunes (ACC) AES Protected Audio
ALAC FileType = "ALAC" // Apple Lossless file FIXME: actually detect this
FLAC FileType = "FLAC" // FLAC file
OGG FileType = "OGG" // OGG file
DSF FileType = "DSF" // DSF file DSD Sony format see https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf
)
// Metadata is an interface which is used to describe metadata retrieved by this package.
type Metadata interface {
// Format returns the metadata Format used to encode the data.
Format() Format
// FileType returns the file type of the audio file.
FileType() FileType
// Title returns the title of the track.
Title() string
// Album returns the album name of the track.
Album() string
// Artist returns the artist name of the track.
Artist() string
// AlbumArtist returns the album artist name of the track.
AlbumArtist() string
// Composer returns the composer of the track.
Composer() string
// Year returns the year of the track.
Year() int
// Genre returns the genre of the track.
Genre() string
// Track returns the track number and total tracks, or zero values if unavailable.
Track() (int, int)
// Disc returns the disc number and total discs, or zero values if unavailable.
Disc() (int, int)
// Picture returns a picture, or nil if not available.
Picture() *Picture
// Lyrics returns the lyrics, or an empty string if unavailable.
Lyrics() string
// Comment returns the comment, or an empty string if unavailable.
Comment() string
// Raw returns the raw mapping of retrieved tag names and associated values.
// NB: tag/atom names are not standardised between formats.
Raw() map[string]interface{}
}

102
vendor/github.com/dhowden/tag/util.go generated vendored Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"bytes"
"encoding/binary"
"io"
)
func getBit(b byte, n uint) bool {
x := byte(1 << n)
return (b & x) == x
}
func get7BitChunkedInt(b []byte) int {
var n int
for _, x := range b {
n = n << 7
n |= int(x)
}
return n
}
func getInt(b []byte) int {
var n int
for _, x := range b {
n = n << 8
n |= int(x)
}
return n
}
func readUint64LittleEndian(r io.Reader) (uint64, error) {
b, err := readBytes(r, 8)
if err != nil {
return 0, err
}
return binary.LittleEndian.Uint64(b), nil
}
// readBytesMaxUpfront is the max up-front allocation allowed
const readBytesMaxUpfront = 10 << 20 // 10MB
func readBytes(r io.Reader, n uint) ([]byte, error) {
if n > readBytesMaxUpfront {
b := &bytes.Buffer{}
if _, err := io.CopyN(b, r, int64(n)); err != nil {
return nil, err
}
return b.Bytes(), nil
}
b := make([]byte, n)
_, err := io.ReadFull(r, b)
if err != nil {
return nil, err
}
return b, nil
}
func readString(r io.Reader, n uint) (string, error) {
b, err := readBytes(r, n)
if err != nil {
return "", err
}
return string(b), nil
}
func readUint(r io.Reader, n uint) (uint, error) {
x, err := readInt(r, n)
if err != nil {
return 0, err
}
return uint(x), nil
}
func readInt(r io.Reader, n uint) (int, error) {
b, err := readBytes(r, n)
if err != nil {
return 0, err
}
return getInt(b), nil
}
func read7BitChunkedUint(r io.Reader, n uint) (uint, error) {
b, err := readBytes(r, n)
if err != nil {
return 0, err
}
return uint(get7BitChunkedInt(b)), nil
}
func readUint32LittleEndian(r io.Reader) (uint32, error) {
b, err := readBytes(r, 4)
if err != nil {
return 0, err
}
return binary.LittleEndian.Uint32(b), nil
}

266
vendor/github.com/dhowden/tag/vorbis.go generated vendored Normal file
View File

@ -0,0 +1,266 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
)
func newMetadataVorbis() *metadataVorbis {
return &metadataVorbis{
c: make(map[string]string),
}
}
type metadataVorbis struct {
c map[string]string // the vorbis comments
p *Picture
}
func (m *metadataVorbis) readVorbisComment(r io.Reader) error {
vendorLen, err := readUint32LittleEndian(r)
if err != nil {
return err
}
vendor, err := readString(r, uint(vendorLen))
if err != nil {
return err
}
m.c["vendor"] = vendor
commentsLen, err := readUint32LittleEndian(r)
if err != nil {
return err
}
for i := uint32(0); i < commentsLen; i++ {
l, err := readUint32LittleEndian(r)
if err != nil {
return err
}
s, err := readString(r, uint(l))
if err != nil {
return err
}
k, v, err := parseComment(s)
if err != nil {
return err
}
m.c[strings.ToLower(k)] = v
}
if b64data, ok := m.c["metadata_block_picture"]; ok {
data, err := base64.StdEncoding.DecodeString(b64data)
if err != nil {
return err
}
m.readPictureBlock(bytes.NewReader(data))
}
return nil
}
func (m *metadataVorbis) readPictureBlock(r io.Reader) error {
b, err := readInt(r, 4)
if err != nil {
return err
}
pictureType, ok := pictureTypes[byte(b)]
if !ok {
return fmt.Errorf("invalid picture type: %v", b)
}
mimeLen, err := readUint(r, 4)
if err != nil {
return err
}
mime, err := readString(r, mimeLen)
if err != nil {
return err
}
ext := ""
switch mime {
case "image/jpeg":
ext = "jpg"
case "image/png":
ext = "png"
case "image/gif":
ext = "gif"
}
descLen, err := readUint(r, 4)
if err != nil {
return err
}
desc, err := readString(r, descLen)
if err != nil {
return err
}
// We skip width <32>, height <32>, colorDepth <32>, coloresUsed <32>
_, err = readInt(r, 4) // width
if err != nil {
return err
}
_, err = readInt(r, 4) // height
if err != nil {
return err
}
_, err = readInt(r, 4) // color depth
if err != nil {
return err
}
_, err = readInt(r, 4) // colors used
if err != nil {
return err
}
dataLen, err := readInt(r, 4)
if err != nil {
return err
}
data := make([]byte, dataLen)
_, err = io.ReadFull(r, data)
if err != nil {
return err
}
m.p = &Picture{
Ext: ext,
MIMEType: mime,
Type: pictureType,
Description: desc,
Data: data,
}
return nil
}
func parseComment(c string) (k, v string, err error) {
kv := strings.SplitN(c, "=", 2)
if len(kv) != 2 {
err = errors.New("vorbis comment must contain '='")
return
}
k = kv[0]
v = kv[1]
return
}
func (m *metadataVorbis) Format() Format {
return VORBIS
}
func (m *metadataVorbis) Raw() map[string]interface{} {
raw := make(map[string]interface{}, len(m.c))
for k, v := range m.c {
raw[k] = v
}
return raw
}
func (m *metadataVorbis) Title() string {
return m.c["title"]
}
func (m *metadataVorbis) Artist() string {
// ARTIST
// The artist generally considered responsible for the work. In popular music
// this is usually the performing band or singer. For classical music it would
// be the composer. For an audio book it would be the author of the original text.
return m.c["artist"]
}
func (m *metadataVorbis) Album() string {
return m.c["album"]
}
func (m *metadataVorbis) AlbumArtist() string {
// This field isn't actually included in the standard, though
// it is commonly assigned to albumartist.
return m.c["albumartist"]
}
func (m *metadataVorbis) Composer() string {
if m.c["composer"] != "" {
return m.c["composer"]
}
// PERFORMER
// The artist(s) who performed the work. In classical music this would be the
// conductor, orchestra, soloists. In an audio book it would be the actor who
// did the reading. In popular music this is typically the same as the ARTIST
// and is omitted.
if m.c["performer"] != "" {
return m.c["performer"]
}
return m.c["artist"]
}
func (m *metadataVorbis) Genre() string {
return m.c["genre"]
}
func (m *metadataVorbis) Year() int {
var dateFormat string
// The date need to follow the international standard https://en.wikipedia.org/wiki/ISO_8601
// and obviously the VorbisComment standard https://wiki.xiph.org/VorbisComment#Date_and_time
switch len(m.c["date"]) {
case 0:
// Fallback on year tag as some files use that.
if len(m.c["year"]) != 0 {
year, err := strconv.Atoi(m.c["year"])
if err == nil {
return year
}
}
return 0
case 4:
dateFormat = "2006"
case 7:
dateFormat = "2006-01"
case 10:
dateFormat = "2006-01-02"
}
t, _ := time.Parse(dateFormat, m.c["date"])
return t.Year()
}
func (m *metadataVorbis) Track() (int, int) {
x, _ := strconv.Atoi(m.c["tracknumber"])
// https://wiki.xiph.org/Field_names
n, _ := strconv.Atoi(m.c["tracktotal"])
return x, n
}
func (m *metadataVorbis) Disc() (int, int) {
// https://wiki.xiph.org/Field_names
x, _ := strconv.Atoi(m.c["discnumber"])
n, _ := strconv.Atoi(m.c["disctotal"])
return x, n
}
func (m *metadataVorbis) Lyrics() string {
return m.c["lyrics"]
}
func (m *metadataVorbis) Comment() string {
if m.c["comment"] != "" {
return m.c["comment"]
}
return m.c["description"]
}
func (m *metadataVorbis) Picture() *Picture {
return m.p
}

3
vendor/modules.txt vendored Normal file
View File

@ -0,0 +1,3 @@
# github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
## explicit; go 1.20
github.com/dhowden/tag