parent
a946d2b966
commit
e1090a7583
@ -0,0 +1,2 @@
|
||||
videos/*
|
||||
!videos/README.md
|
@ -0,0 +1,21 @@
|
||||
# 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/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "ed099d42384823742bba0bf9a72b53b55c9e2e38"
|
||||
version = "v1.7.2"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "46ffc0b430cdc7896ae1d51e97b0698ca0cf1df4a0de7ce14e644fa45848491f"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
@ -0,0 +1,38 @@
|
||||
# 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,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/wybiral/tube/pkg/media"
|
||||
)
|
||||
|
||||
const addr = "127.0.0.1:40404"
|
||||
|
||||
var templates *template.Template
|
||||
|
||||
var library *media.Library
|
||||
var playlist media.Playlist
|
||||
|
||||
func main() {
|
||||
library = media.NewLibrary()
|
||||
err := library.Import("./videos")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
playlist = library.Playlist()
|
||||
if len(playlist) == 0 {
|
||||
log.Fatal("No valid videos found")
|
||||
}
|
||||
templates = template.Must(template.ParseGlob("templates/*"))
|
||||
r := mux.NewRouter().StrictSlash(true)
|
||||
r.HandleFunc("/", index).Methods("GET")
|
||||
r.HandleFunc("/v/{id}.mp4", video).Methods("GET")
|
||||
r.HandleFunc("/t/{id}", thumb).Methods("GET")
|
||||
r.HandleFunc("/{id}", page).Methods("GET")
|
||||
r.PathPrefix("/static/").Handler(
|
||||
http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))),
|
||||
).Methods("GET")
|
||||
log.Printf("Serving at %s", addr)
|
||||
http.ListenAndServe(addr, r)
|
||||
}
|
||||
|
||||
func index(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("/index")
|
||||
http.Redirect(w, r, "/"+playlist[0].ID, 302)
|
||||
}
|
||||
|
||||
func page(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
log.Printf(id)
|
||||
playing, ok := library.Videos[id]
|
||||
if !ok {
|
||||
log.Print(ok)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
templates.ExecuteTemplate(w, "index.html", &struct {
|
||||
Playing *media.Video
|
||||
Playlist media.Playlist
|
||||
}{
|
||||
Playing: playing,
|
||||
Playlist: playlist,
|
||||
})
|
||||
}
|
||||
|
||||
func video(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
log.Print("/v/" + id)
|
||||
m, ok := library.Videos[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
title := m.Title
|
||||
disposition := "attachment; filename=\"" + title + ".mp4\""
|
||||
w.Header().Set("Content-Disposition", disposition)
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
http.ServeFile(w, r, "./videos/"+id+".mp4")
|
||||
}
|
||||
|
||||
func thumb(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
log.Printf("/t/" + id)
|
||||
m, ok := library.Videos[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "public, max-age=7776000")
|
||||
if m.ThumbType == "" {
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
http.ServeFile(w, r, "static/defaulticon.jpg")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", m.ThumbType)
|
||||
w.Write(m.Thumb)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package media
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Library struct {
|
||||
Videos map[string]*Video
|
||||
}
|
||||
|
||||
func NewLibrary() *Library {
|
||||
lib := &Library{
|
||||
Videos: make(map[string]*Video),
|
||||
}
|
||||
return lib
|
||||
}
|
||||
|
||||
func (lib *Library) Import(path string) error {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, info := range files {
|
||||
name := info.Name()
|
||||
v, err := ParseVideo(path + "/" + name)
|
||||
if err != nil {
|
||||
// Ignore files that can't be parsed
|
||||
continue
|
||||
}
|
||||
// Set modified date property
|
||||
v.Modified = info.ModTime().Format("2006-01-02")
|
||||
// Default title is filename
|
||||
if v.Title == "" {
|
||||
v.Title = name
|
||||
}
|
||||
// ID is name without extension
|
||||
idx := strings.LastIndex(name, ".")
|
||||
if idx == -1 {
|
||||
idx = len(name)
|
||||
}
|
||||
v.ID = name[:idx]
|
||||
lib.Videos[v.ID] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lib *Library) Playlist() Playlist {
|
||||
pl := make(Playlist, 0)
|
||||
for _, v := range lib.Videos {
|
||||
pl = append(pl, v)
|
||||
}
|
||||
sort.Sort(pl)
|
||||
return pl
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package media
|
||||
|
||||
type Playlist []*Video
|
||||
|
||||
func (p Playlist) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
func (p Playlist) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
|
||||
func (p Playlist) Less(i, j int) bool {
|
||||
return p[i].ID < p[j].ID
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package media
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
type Video struct {
|
||||
ID string
|
||||
Title string
|
||||
Album string
|
||||
Description string
|
||||
Thumb []byte
|
||||
ThumbType string
|
||||
Modified string
|
||||
}
|
||||
|
||||
func ParseVideo(path string) (*Video, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := tag.ReadFrom(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := &Video{
|
||||
Title: m.Title(),
|
||||
Album: m.Album(),
|
||||
Description: m.Comment(),
|
||||
}
|
||||
// Add thumbnail (if exists)
|
||||
p := m.Picture()
|
||||
if p != nil {
|
||||
v.Thumb = p.Data
|
||||
v.ThumbType = p.MIMEType
|
||||
}
|
||||
return v, nil
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/* Normalize */
|
||||
* {
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #c5c8c6;
|
||||
background: #1e1e1e;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
nav {
|
||||
z-index: 100;
|
||||
color: #ae81ff;
|
||||
text-shadow: -2px 2px 3px rgba(0, 0, 0, 0.7);
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
text-indent: 20px;
|
||||
line-height: 50px;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #171717;
|
||||
border-bottom: 1px solid #272727;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 1156px;
|
||||
margin:0 auto;
|
||||
margin-top: 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#player {
|
||||
width: 856px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#video {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
background: #000;
|
||||
box-shadow: 0 3px 7px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#player > h1 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#player > h2 {
|
||||
margin-top: 5px;
|
||||
color: #676867;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
#player > p {
|
||||
margin-top: 10px;
|
||||
font-size: 80%;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 290px;
|
||||
background: #282a2e;
|
||||
box-shadow: 0 3px 7px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#sidebar > a {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#sidebar > a:hover {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
#sidebar > a.playing {
|
||||
background: #383a3e;
|
||||
}
|
||||
|
||||
#sidebar > a + a {
|
||||
border-top: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
#sidebar > a > img {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
#sidebar > a > div {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
left: 90px;
|
||||
}
|
||||
|
||||
#sidebar > a > div > h1 {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#sidebar > a > div > h2 {
|
||||
margin-top: 5px;
|
||||
color: #676867;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1156px) {
|
||||
main {
|
||||
width: 940px;
|
||||
}
|
||||
#player {
|
||||
width: 640px;
|
||||
}
|
||||
#video {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 940px) {
|
||||
main {
|
||||
width: 726px;
|
||||
}
|
||||
#player {
|
||||
width: 426px;
|
||||
}
|
||||
#video {
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 726px) {
|
||||
main {
|
||||
width: 426px;
|
||||
}
|
||||
#player {
|
||||
width: 426px;
|
||||
}
|
||||
#video {
|
||||
height: 240px;
|
||||
}
|
||||
#sidebar {
|
||||
width: 426px;
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,36 @@
|
||||
{{ $playing := .Playing }}
|
||||
<html>
|
||||
<head>
|
||||
<title>Tube</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav><a href="/">Tube</a></nav>
|
||||
<main>
|
||||
<div id="player">
|
||||
<video id="video" controls poster="/t/{{ $playing.ID}}" src="/v/{{ $playing.ID }}.mp4"></video>
|
||||
<h1>{{ $playing.Title }}</h1>
|
||||
<h2>{{ $playing.Modified }}</h2>
|
||||
<p>{{ $playing.Description }}</p>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
{{ range $m := .Playlist }}
|
||||
{{ if eq $m.ID $playing.ID }}
|
||||
<a href="/{{ $m.ID }}" class="playing">
|
||||
{{ else }}
|
||||
<a href="/{{ $m.ID }}">
|
||||
{{ end }}
|
||||
<img src="/t/{{ $m.ID }}">
|
||||
<div>
|
||||
<h1>{{ $m.Title }}</h1>
|
||||
<h2>{{ $m.Modified }}</h2>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -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
|
@ -0,0 +1,5 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.7
|
||||
- tip
|
@ -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.
|
@ -0,0 +1,72 @@
|
||||
# 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
|
||||
```
|
@ -0,0 +1,110 @@
|
||||
// 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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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) }
|
@ -0,0 +1,434 @@
|
||||
// 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)
|
||||
}
|
@ -0,0 +1,638 @@
|
||||
// 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",
|
||||