MeteorLight/util/http.go

299 lines
6.3 KiB
Go

package util
import (
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"strconv"
"sync"
)
const rangeReaderBufferSize = 1024 * 16
type RangeReadSeekCloser struct {
uri *url.URL
size int64
readOffset int64
body io.ReadCloser
bodyLock sync.RWMutex
buf []byte
bufferOffset int64
closed bool
fileName string
}
func NewRangeReadSeekCloser(uri string) (*RangeReadSeekCloser, error) {
parsedUrl, err := url.Parse(uri)
if err != nil {
return nil, err
}
r := &RangeReadSeekCloser{
uri: parsedUrl,
}
if err = r.getInformation(); err != nil {
return nil, err
}
return r, nil
}
func (r *RangeReadSeekCloser) GetURI() string {
return r.uri.String()
}
func (r *RangeReadSeekCloser) getClient() *http.Client {
return &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
req.Header.Del("referer")
return nil
},
}
}
func (r *RangeReadSeekCloser) retryConnect() error {
r.bodyLock.Lock()
defer r.bodyLock.Unlock()
if r.closed {
return errors.New("already closed")
}
headers := make(http.Header)
startOffset := r.readOffset
expectedLength := r.size - startOffset
headers.Set("Range", fmt.Sprintf("bytes=%d-", startOffset))
headers.Set("User-Agent", "MeteorLight/http-reader")
response, err := r.getClient().Do(&http.Request{
Method: "GET",
URL: r.uri,
Header: headers,
})
if err != nil {
return err
}
r.body = response.Body
if response.StatusCode != http.StatusPartialContent {
_ = r.body.Close()
r.body = nil
return fmt.Errorf("response status code %d != %d", response.StatusCode, http.StatusPartialContent)
}
contentLength, err := strconv.ParseInt(response.Header.Get("content-length"), 10, 0)
if err != nil {
_ = r.body.Close()
r.body = nil
return errors.New("server response does not have a valid Content-Length")
}
if contentLength != expectedLength {
_ = r.body.Close()
r.body = nil
return fmt.Errorf("server returned %d bytes, expected %d", contentLength, expectedLength)
}
return nil
}
func (r *RangeReadSeekCloser) Read(p []byte) (n int, err error) {
r.bodyLock.RLock()
defer r.bodyLock.RUnlock()
for retry := 0; retry < 2; retry++ {
if r.readOffset >= r.size {
return 0, io.EOF
}
if r.readOffset >= r.bufferOffset {
bufStart := r.readOffset - r.bufferOffset
if bufStart <= int64(len(r.buf)) {
bufEnd := bufStart + int64(len(p))
if bufEnd >= int64(len(r.buf)) {
bufEnd = int64(len(r.buf))
}
if bufEnd-bufStart > 0 {
copy(p, r.buf[bufStart:bufEnd])
r.readOffset += bufEnd - bufStart
return int(bufEnd - bufStart), nil
}
}
}
if r.body == nil {
if err = func() error {
r.bodyLock.RUnlock()
defer r.bodyLock.RLock()
return r.retryConnect()
}(); err != nil {
continue
}
}
data := make([]byte, rangeReaderBufferSize)
n, err = r.body.Read(data)
if err != nil { //detect actual eof, not a disconnection
if err == io.EOF || err == io.ErrUnexpectedEOF {
if r.readOffset+int64(n) != r.size {
continue
}
} else {
continue
}
}
r.buf = data[:n]
r.bufferOffset = r.readOffset
readBytes := len(p)
if n < readBytes {
readBytes = n
}
copy(p[:readBytes], data[:readBytes])
r.readOffset += int64(readBytes)
return readBytes, nil
}
return 0, errors.New("could not retry")
}
func (r *RangeReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
r.bodyLock.Lock()
defer r.bodyLock.Unlock()
oldOffset := r.readOffset
switch whence {
case io.SeekStart:
r.readOffset = offset
case io.SeekCurrent:
r.readOffset += offset
case io.SeekEnd:
//todo: maybe without -1?
r.readOffset = (r.size - 1) - offset
default:
return 0, fmt.Errorf("unknown whence %d", whence)
}
if oldOffset != r.readOffset {
if r.body != nil {
_ = r.body.Close()
}
r.body = nil
}
if r.readOffset >= r.size {
return r.readOffset, io.EOF
} else if r.readOffset < 0 {
return r.readOffset, io.ErrUnexpectedEOF
}
return r.readOffset, nil
}
func (r *RangeReadSeekCloser) Close() error {
r.bodyLock.Lock()
defer r.bodyLock.Unlock()
if r.body != nil {
r.closed = true
defer func() {
r.body = nil
}()
return r.body.Close()
}
return nil
}
func (r *RangeReadSeekCloser) GetFileName() string {
return r.fileName
}
func (r *RangeReadSeekCloser) getInformation() error {
headers := make(http.Header)
headers.Set("Range", "bytes=0-")
headers.Set("User-Agent", "MeteorLight/http-reader")
response, err := r.getClient().Do(&http.Request{
Method: "HEAD",
URL: r.uri,
Header: headers,
})
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusPartialContent {
return fmt.Errorf("response status code %d != %d, server might not accept Range", response.StatusCode, http.StatusPartialContent)
}
if r.size, err = strconv.ParseInt(response.Header.Get("content-length"), 10, 0); err != nil || r.size <= 0 {
return errors.New("server response does not have a valid Content-Length")
}
if response.Header.Get("content-disposition") != "" {
if _, params, err := mime.ParseMediaType(response.Header.Get("content-disposition")); err == nil {
if disposition, ok := params["filename"]; ok {
r.fileName = disposition
}
}
}
if r.fileName == "" {
if response.Header.Get("content-type") != "" {
if mimeType, params, err := mime.ParseMediaType(response.Header.Get("content-type")); err == nil {
switch mimeType {
case "audio/flac":
r.fileName = "_.flac"
case "audio/ogg":
r.fileName = "_.ogg"
if codecs, ok := params["codecs"]; ok {
switch codecs {
case "vorbis":
r.fileName = "_.vorbis"
case "opus":
r.fileName = "_.opus"
case "flac":
r.fileName = "_.flac"
}
}
case "audio/mpeg":
r.fileName = "_.mp3"
if codecs, ok := params["codecs"]; ok {
switch codecs {
case "mp3":
r.fileName = "_.mp3"
}
}
case "audio/mp4":
r.fileName = "_.m4a"
if codecs, ok := params["codecs"]; ok {
switch codecs {
case "alac":
r.fileName = "_.alac"
}
}
case "audio/aac":
r.fileName = "_.aac"
case "audio/x-tta", "audio/tta":
r.fileName = "_.tta"
}
}
}
}
if r.fileName == "" {
r.fileName = r.GetURI()
}
return nil
}