parent
13ffd71171
commit
d7a1eab5be
@ -1,4 +1,9 @@
|
||||
*~
|
||||
*.bak
|
||||
*.key
|
||||
|
||||
/dist
|
||||
/tube
|
||||
|
||||
videos/*
|
||||
!videos/README.md
|
||||
onion.key
|
||||
build/bin
|
@ -0,0 +1,35 @@
|
||||
---
|
||||
builds:
|
||||
-
|
||||
flags: -tags "static_build"
|
||||
ldflags: -w -X github.com/prologic/tube/.Version={{.Version}} -X github.com/prologic/tube/.Commit={{.Commit}}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
- freebsd
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
sign:
|
||||
artifacts: checksum
|
||||
archive:
|
||||
replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
@ -0,0 +1,29 @@
|
||||
---
|
||||
yaml-files:
|
||||
- '*.yaml'
|
||||
- '*.yml'
|
||||
- '.yamllint'
|
||||
|
||||
rules:
|
||||
braces: enable
|
||||
brackets: enable
|
||||
colons: enable
|
||||
commas: enable
|
||||
comments: disable
|
||||
comments-indentation: disable
|
||||
document-end: disable
|
||||
document-start:
|
||||
level: warning
|
||||
empty-lines: enable
|
||||
empty-values: disable
|
||||
hyphens: enable
|
||||
indentation: enable
|
||||
key-duplicates: enable
|
||||
key-ordering: disable
|
||||
line-length: disable
|
||||
new-line-at-end-of-file: enable
|
||||
new-lines: enable
|
||||
octal-values: enable
|
||||
quoted-strings: disable
|
||||
trailing-spaces: enable
|
||||
truthy: disable
|
@ -0,0 +1,10 @@
|
||||
# Build
|
||||
FROM prologic/go-builder:latest AS build
|
||||
|
||||
# Runtime
|
||||
FROM alpine
|
||||
|
||||
COPY --from=build /src/tube /tube
|
||||
|
||||
ENTRYPOINT ["/tube"]
|
||||
CMD [""]
|
@ -1,67 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dhowden/tag"
|
||||
packages = ["."]
|
||||
revision = "db0c67e351b1bfbdfc4f99c911e8afd0ca67de98"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
|
||||
version = "v1.4.7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/feeds"
|
||||
packages = ["."]
|
||||
revision = "2079b9bbce59c062ad62eecb771e3ed3a772b8e4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "ed099d42384823742bba0bf9a72b53b55c9e2e38"
|
||||
version = "v1.7.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/wybiral/torgo"
|
||||
packages = ["."]
|
||||
revision = "a19a6c8a50489e2fdf37223edc5f1284bb965308"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"sha3"
|
||||
]
|
||||
revision = "4def268fd1a49955bfb3dda92fe3db4f924f2285"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"internal/socks",
|
||||
"proxy"
|
||||
]
|
||||
revision = "da137c7871d730100384dbcf36e6f8fa493aef5b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"cpu",
|
||||
"unix"
|
||||
]
|
||||
revision = "04f50cda93cbb67f2afa353c52f342100e80e625"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "c0b13b7e13cee43839881abf059f0b69ef9385100bcad825291bce099a60c3d8"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
@ -1,38 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dhowden/tag"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/mux"
|
||||
version = "1.7.2"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
@ -0,0 +1,28 @@
|
||||
.PHONY: dev build install test release clean
|
||||
|
||||
CGO_ENABLED=0
|
||||
VERSION=$(shell git describe --abbrev=0 --tags)
|
||||
COMMIT=$(shell git rev-parse --short HEAD)
|
||||
|
||||
all: dev
|
||||
|
||||
dev: build
|
||||
@./tube
|
||||
|
||||
build: clean
|
||||
@go build \
|
||||
-tags "netgo static_build" -installsuffix netgo \
|
||||
-ldflags "-w -X $(shell go list).Version=$(VERSION) -X $(shell go list).Commit=$(COMMIT)" \
|
||||
.
|
||||
|
||||
install: build
|
||||
@go install
|
||||
|
||||
test: install
|
||||
@go test
|
||||
|
||||
release:
|
||||
@./tools/release.sh
|
||||
|
||||
clean:
|
||||
@git clean -f -d -X
|
@ -1,54 +0,0 @@
|
||||
# Build Go project for different platforms and create zip archive of project
|
||||
# directory structure.
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
def build(pkg, bin, env):
|
||||
src = 'github.com/wybiral/tube'
|
||||
x = os.system('{env} go build -o bin/{bin} {src}'.format(
|
||||
env=env,
|
||||
bin=bin,
|
||||
src=src,
|
||||
))
|
||||
if x != 0:
|
||||
print('Error building ' + pkg)
|
||||
return
|
||||
z = zipfile.ZipFile('bin/' + pkg, mode='w')
|
||||
z.write('bin/' + bin, arcname=bin)
|
||||
z.write('../config.json', arcname='config.json')
|
||||
z.write('../README.md', 'README.md')
|
||||
z.write('../videos/README.md', 'videos/README.md')
|
||||
for filename in os.listdir('../static'):
|
||||
z.write('../static/' + filename, 'static/' + filename)
|
||||
for filename in os.listdir('../templates'):
|
||||
z.write('../templates/' + filename, 'templates/' + filename)
|
||||
# cleanup executable
|
||||
os.remove('bin/' + bin)
|
||||
print('Built ' + pkg)
|
||||
|
||||
build(
|
||||
pkg='tube_linux.zip',
|
||||
bin='tube',
|
||||
env='GOOS=linux GOARCH=amd64',
|
||||
)
|
||||
|
||||
build(
|
||||
pkg='tube_windows.zip',
|
||||
bin='tube.exe',
|
||||
env='GOOS=windows GOARCH=amd64',
|
||||
)
|
||||
|
||||
build(
|
||||
pkg='tube_osx.zip',
|
||||
bin='tube',
|
||||
env='GOOS=darwin GOARCH=amd64',
|
||||
)
|
||||
|
||||
build(
|
||||
pkg='tube_arm6.zip',
|
||||
bin='tube',
|
||||
env='GOOS=linux GOARCH=arm GOARM=6',
|
||||
)
|
||||
|
||||
print('Done.')
|
@ -0,0 +1,15 @@
|
||||
module github.com/wybiral/tube
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/dhowden/tag v0.0.0-20190519100835-db0c67e351b1
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/gorilla/mux v1.7.2
|
||||
github.com/wybiral/feeds v1.1.1
|
||||
github.com/wybiral/torgo v0.0.0-20190413024533-a19a6c8a5048
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb
|
||||
)
|
@ -0,0 +1,22 @@
|
||||
github.com/dhowden/tag v0.0.0-20190519100835-db0c67e351b1 h1:HR8W6GvuS20j4kNxa/XQeyVA0vHLKVMCAVJj0RGWauY=
|
||||
github.com/dhowden/tag v0.0.0-20190519100835-db0c67e351b1/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/wybiral/feeds v1.1.1 h1:KWE/JxA2XfP0My+C0wymqXrWK5oIyjRVbH5kpclsea0=
|
||||
github.com/wybiral/feeds v1.1.1/go.mod h1:MRSqtY+Oy5HMM51cF212xqU39MYbsYq/AH+PY8IfeM0=
|
||||
github.com/wybiral/torgo v0.0.0-20190413024533-a19a6c8a5048 h1:HQmSLHtGgSARs7VYzAID703qM/5m1il3X7f9AHfg/9c=
|
||||
github.com/wybiral/torgo v0.0.0-20190413024533-a19a6c8a5048/go.mod h1:LAhGyZRjuXZ/+uO4tqc5QV26hkdIo+yGHPfX1aubR0M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Get the highest tag number
|
||||
VERSION="$(git describe --abbrev=0 --tags)"
|
||||
VERSION=${VERSION:-'0.0.0'}
|
||||
|
||||
# Get number parts
|
||||
MAJOR="${VERSION%%.*}"; VERSION="${VERSION#*.}"
|
||||
MINOR="${VERSION%%.*}"; VERSION="${VERSION#*.}"
|
||||
PATCH="${VERSION%%.*}"; VERSION="${VERSION#*.}"
|
||||
|
||||
# Increase version
|
||||
PATCH=$((PATCH+1))
|
||||
|
||||
TAG="${1}"
|
||||
|
||||
if [ "${TAG}" = "" ]; then
|
||||
TAG="${MAJOR}.${MINOR}.${PATCH}"
|
||||
fi
|
||||
|
||||
echo "Releasing ${TAG} ..."
|
||||
|
||||
git tag -a -s -m "Release ${TAG}" "${TAG}"
|
||||
git push --tags
|
||||
goreleaser release --rm-dist
|
@ -1,19 +0,0 @@
|
||||
# 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
|
@ -1,5 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.7
|
||||
- tip
|
@ -1,23 +0,0 @@
|
||||
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.
|
@ -1,72 +0,0 @@
|
||||
# MP3/MP4/OGG/FLAC metadata parsing library
|
||||
[](https://travis-ci.org/dhowden/tag)
|
||||
[](https://godoc.org/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.
|
||||
|
||||
[http://godoc.org/github.com/dhowden/tag#Sum](http://godoc.org/github.com/dhowden/tag#Sum)
|
||||
|
||||
## Tools
|
||||
|
||||
There are simple command-line tools which demonstrate basic tag extraction and summing:
|
||||
|
||||
```console
|
||||
$ go get github.com/dhowden/tag/...
|
||||
$ 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
|
||||
```
|
@ -1,110 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
n4, err := readBytes(r, 8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id3Pointer := getIntLittleEndian(n4)
|
||||
|
||||
_, 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()
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
// 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
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
// 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) }
|
@ -1,434 +0,0 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
// id3v2Header is a type which represents an ID3v2 tag header.
|
||||
type id3v2Header struct {
|
||||
Version Format
|
||||
Unsynchronisation bool
|
||||
ExtendedHeader bool
|
||||
Experimental bool
|
||||
Size int
|
||||
}
|
||||
|
||||
// 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 int, 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: 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 := 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 := 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 int, headerSize int, err error) {
|
||||
name, err = readString(r, 3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size, err = readInt(r, 3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
headerSize = 6
|
||||
return
|
||||
}
|
||||
|
||||
func readID3v2_3FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
||||
name, err = readString(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size, err = readInt(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
headerSize = 8
|
||||
return
|
||||
}
|
||||
|
||||
func readID3v2_4FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
||||
name, err = readString(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size, err = read7BitChunkedInt(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 int, h *id3v2Header) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for offset < h.Size {
|
||||
var err error
|
||||
var name string
|
||||
var size, headerSize int
|
||||
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 {
|
||||
_, err = read7BitChunkedInt(r, 4) // read 4
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size -= 4
|
||||
}
|
||||
|
||||
if flags.Encryption {
|
||||
_, err = readBytes(r, 1) // read 1 byte of encryption method
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size -= 1
|
||||
}
|
||||
}
|
||||
|
||||
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, 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)
|
||||
}
|
@ -1,638 +0,0 @@
|
||||
// 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",
|