2022-09-02 07:44:54 +00:00
|
|
|
package util
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2022-08-02 19:24:43 +00:00
|
|
|
"mime"
|
2022-03-06 16:55:34 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
2022-08-01 12:31:37 +00:00
|
|
|
"sync"
|
2022-03-06 16:55:34 +00:00
|
|
|
)
|
|
|
|
|
2022-08-01 12:31:37 +00:00
|
|
|
const rangeReaderBufferSize = 1024 * 16
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
type RangeReadSeekCloser struct {
|
2022-08-01 12:31:37 +00:00
|
|
|
uri *url.URL
|
|
|
|
size int64
|
|
|
|
readOffset int64
|
|
|
|
body io.ReadCloser
|
|
|
|
bodyLock sync.RWMutex
|
|
|
|
buf []byte
|
|
|
|
bufferOffset int64
|
|
|
|
closed bool
|
2022-08-02 19:24:43 +00:00
|
|
|
fileName string
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-08-18 08:28:46 +00:00
|
|
|
func (r *RangeReadSeekCloser) getClient() *http.Client {
|
|
|
|
return &http.Client{
|
|
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
|
|
req.Header.Del("referer")
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-06 19:19:36 +00:00
|
|
|
func (r *RangeReadSeekCloser) retryConnect() error {
|
2022-08-01 12:31:37 +00:00
|
|
|
r.bodyLock.Lock()
|
|
|
|
defer r.bodyLock.Unlock()
|
2022-03-07 15:19:38 +00:00
|
|
|
if r.closed {
|
|
|
|
return errors.New("already closed")
|
|
|
|
}
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
headers := make(http.Header)
|
2022-08-01 12:31:37 +00:00
|
|
|
startOffset := r.readOffset
|
2022-03-06 16:55:34 +00:00
|
|
|
|
2022-03-06 19:19:36 +00:00
|
|
|
expectedLength := r.size - startOffset
|
|
|
|
headers.Set("Range", fmt.Sprintf("bytes=%d-", startOffset))
|
2022-09-02 07:44:54 +00:00
|
|
|
headers.Set("User-Agent", "MeteorLight/http-reader")
|
2022-08-18 08:28:46 +00:00
|
|
|
response, err := r.getClient().Do(&http.Request{
|
2022-03-06 16:55:34 +00:00
|
|
|
Method: "GET",
|
|
|
|
URL: r.uri,
|
|
|
|
Header: headers,
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
2022-03-06 19:19:36 +00:00
|
|
|
return err
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
2022-03-06 19:19:36 +00:00
|
|
|
r.body = response.Body
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
if response.StatusCode != http.StatusPartialContent {
|
2022-10-03 09:56:54 +00:00
|
|
|
_ = r.body.Close()
|
2022-03-06 19:19:36 +00:00
|
|
|
r.body = nil
|
|
|
|
return fmt.Errorf("response status code %d != %d", response.StatusCode, http.StatusPartialContent)
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
contentLength, err := strconv.ParseInt(response.Header.Get("content-length"), 10, 0)
|
|
|
|
if err != nil {
|
2022-10-03 09:56:54 +00:00
|
|
|
_ = r.body.Close()
|
2022-03-06 19:19:36 +00:00
|
|
|
r.body = nil
|
|
|
|
return errors.New("server response does not have a valid Content-Length")
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if contentLength != expectedLength {
|
2022-10-03 09:56:54 +00:00
|
|
|
_ = r.body.Close()
|
2022-03-06 19:19:36 +00:00
|
|
|
r.body = nil
|
|
|
|
return fmt.Errorf("server returned %d bytes, expected %d", contentLength, expectedLength)
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
|
|
|
|
2022-03-06 19:19:36 +00:00
|
|
|
return nil
|
|
|
|
}
|
2022-03-06 16:55:34 +00:00
|
|
|
|
2022-03-06 19:19:36 +00:00
|
|
|
func (r *RangeReadSeekCloser) Read(p []byte) (n int, err error) {
|
2022-08-01 12:31:37 +00:00
|
|
|
r.bodyLock.RLock()
|
|
|
|
defer r.bodyLock.RUnlock()
|
|
|
|
|
2022-03-06 19:19:36 +00:00
|
|
|
for retry := 0; retry < 2; retry++ {
|
2022-08-01 12:31:37 +00:00
|
|
|
if r.readOffset >= r.size {
|
2022-03-06 19:19:36 +00:00
|
|
|
return 0, io.EOF
|
|
|
|
}
|
2022-03-06 16:55:34 +00:00
|
|
|
|
2022-08-01 12:31:37 +00:00
|
|
|
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
|
|
|
|
}
|
2022-03-06 19:19:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.body == nil {
|
2022-08-01 12:31:37 +00:00
|
|
|
if err = func() error {
|
|
|
|
r.bodyLock.RUnlock()
|
|
|
|
defer r.bodyLock.RLock()
|
|
|
|
return r.retryConnect()
|
|
|
|
}(); err != nil {
|
2022-03-06 19:19:36 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2022-03-06 16:55:34 +00:00
|
|
|
|
2022-03-06 19:19:36 +00:00
|
|
|
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 {
|
2022-08-01 12:31:37 +00:00
|
|
|
if r.readOffset+int64(n) != r.size {
|
2022-03-06 19:19:36 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
r.buf = data[:n]
|
2022-08-01 12:31:37 +00:00
|
|
|
r.bufferOffset = r.readOffset
|
2022-03-06 19:19:36 +00:00
|
|
|
|
|
|
|
readBytes := len(p)
|
|
|
|
if n < readBytes {
|
|
|
|
readBytes = n
|
|
|
|
}
|
|
|
|
|
|
|
|
copy(p[:readBytes], data[:readBytes])
|
2022-08-01 12:31:37 +00:00
|
|
|
r.readOffset += int64(readBytes)
|
2022-03-06 19:19:36 +00:00
|
|
|
return readBytes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0, errors.New("could not retry")
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RangeReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
|
2022-08-01 12:31:37 +00:00
|
|
|
r.bodyLock.Lock()
|
|
|
|
defer r.bodyLock.Unlock()
|
|
|
|
|
|
|
|
oldOffset := r.readOffset
|
|
|
|
|
2022-03-06 16:55:34 +00:00
|
|
|
switch whence {
|
|
|
|
case io.SeekStart:
|
2022-08-01 12:31:37 +00:00
|
|
|
r.readOffset = offset
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
case io.SeekCurrent:
|
2022-08-01 12:31:37 +00:00
|
|
|
r.readOffset += offset
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
case io.SeekEnd:
|
|
|
|
//todo: maybe without -1?
|
2022-08-01 12:31:37 +00:00
|
|
|
r.readOffset = (r.size - 1) - offset
|
2022-03-06 16:55:34 +00:00
|
|
|
default:
|
|
|
|
return 0, fmt.Errorf("unknown whence %d", whence)
|
|
|
|
}
|
|
|
|
|
2022-08-01 12:31:37 +00:00
|
|
|
if oldOffset != r.readOffset {
|
2022-05-21 14:02:03 +00:00
|
|
|
if r.body != nil {
|
2022-10-03 09:56:54 +00:00
|
|
|
_ = r.body.Close()
|
2022-05-21 14:02:03 +00:00
|
|
|
}
|
2022-03-06 19:19:36 +00:00
|
|
|
r.body = nil
|
|
|
|
}
|
|
|
|
|
2022-08-01 12:31:37 +00:00
|
|
|
if r.readOffset >= r.size {
|
|
|
|
return r.readOffset, io.EOF
|
|
|
|
} else if r.readOffset < 0 {
|
|
|
|
return r.readOffset, io.ErrUnexpectedEOF
|
|
|
|
}
|
|
|
|
|
|
|
|
return r.readOffset, nil
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RangeReadSeekCloser) Close() error {
|
2022-08-01 12:31:37 +00:00
|
|
|
r.bodyLock.Lock()
|
|
|
|
defer r.bodyLock.Unlock()
|
2022-03-06 19:19:36 +00:00
|
|
|
if r.body != nil {
|
2022-03-07 15:19:38 +00:00
|
|
|
r.closed = true
|
2022-03-06 19:19:36 +00:00
|
|
|
defer func() {
|
|
|
|
r.body = nil
|
|
|
|
}()
|
|
|
|
return r.body.Close()
|
|
|
|
}
|
2022-03-06 16:55:34 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-08-02 19:24:43 +00:00
|
|
|
func (r *RangeReadSeekCloser) GetFileName() string {
|
|
|
|
return r.fileName
|
|
|
|
}
|
|
|
|
|
2022-03-06 16:55:34 +00:00
|
|
|
func (r *RangeReadSeekCloser) getInformation() error {
|
2022-09-02 07:44:54 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
|
2022-03-06 16:55:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
2022-09-02 07:44:54 +00:00
|
|
|
if response.StatusCode != http.StatusPartialContent {
|
|
|
|
return fmt.Errorf("response status code %d != %d, server might not accept Range", response.StatusCode, http.StatusPartialContent)
|
2022-03-06 16:55:34 +00:00
|
|
|
}
|
2022-09-02 07:44:54 +00:00
|
|
|
|
|
|
|
if r.size, err = strconv.ParseInt(response.Header.Get("content-length"), 10, 0); err != nil || r.size <= 0 {
|
2022-03-06 16:55:34 +00:00
|
|
|
return errors.New("server response does not have a valid Content-Length")
|
|
|
|
}
|
2022-08-02 19:24:43 +00:00
|
|
|
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()
|
|
|
|
}
|
2022-03-06 16:55:34 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|