mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2026-03-22 00:28:12 +05:00
456 lines
13 KiB
Go
456 lines
13 KiB
Go
package restream
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"image"
|
|
"strings"
|
|
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
|
|
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
|
"git.maid.zone/stuff/soundcloak/lib/misc"
|
|
"git.maid.zone/stuff/soundcloak/lib/preferences"
|
|
"git.maid.zone/stuff/soundcloak/lib/sc"
|
|
"github.com/bogem/id3v2/v2"
|
|
"github.com/gcottom/mp4meta"
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
var image_httpc *fasthttp.HostClient
|
|
var crcTable [256]uint32
|
|
|
|
func Load(r *fiber.App) {
|
|
for i := range crcTable {
|
|
r := uint32(i) << 24
|
|
for j := 0; j < 8; j++ {
|
|
if r&0x80000000 != 0 {
|
|
r = (r << 1) ^ 0x04c11db7
|
|
} else {
|
|
r <<= 1
|
|
}
|
|
}
|
|
crcTable[i] = r
|
|
}
|
|
|
|
image_httpc = &fasthttp.HostClient{
|
|
Addr: cfg.ImageCDN + ":443",
|
|
IsTLS: true,
|
|
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
|
DialDualStack: cfg.DialDualStack,
|
|
}
|
|
|
|
r.Get("/_/restream/:author/:track", func(c fiber.Ctx) error {
|
|
p, err := preferences.Get(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ProxyImages = &cfg.False
|
|
p.ProxyStreams = &cfg.False
|
|
|
|
cid, err := sc.GetClientID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
t, err := sc.GetTrack(cid, c.Params("author")+"/"+c.Params("track"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var isDownload = string(c.RequestCtx().QueryArgs().Peek("metadata")) == "true"
|
|
var forcedQuality = c.RequestCtx().QueryArgs().Peek("audio")
|
|
var quality string
|
|
if len(forcedQuality) != 0 {
|
|
quality = cfg.B2s(forcedQuality)
|
|
} else {
|
|
if isDownload {
|
|
quality = *p.DownloadAudio
|
|
} else {
|
|
quality = *p.RestreamAudio
|
|
}
|
|
}
|
|
|
|
if isDownload {
|
|
var s []byte
|
|
if s = c.RequestCtx().QueryArgs().Peek("title"); len(s) > 0 {
|
|
t.Title = cfg.B2s(s)
|
|
}
|
|
if s = c.RequestCtx().QueryArgs().Peek("genre"); len(s) > 0 {
|
|
t.Genre = cfg.B2s(s)
|
|
}
|
|
if s = c.RequestCtx().QueryArgs().Peek("author"); len(s) > 0 {
|
|
t.Author.Username = cfg.B2s(s)
|
|
}
|
|
}
|
|
|
|
tr, audio := t.Media.SelectCompatible(quality, true, true)
|
|
if tr == nil {
|
|
return fiber.ErrExpectationFailed
|
|
}
|
|
|
|
u, err := tr.GetStream(cid, p, t.Authorization)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Response().Header.SetContentType(tr.Format.MimeType)
|
|
c.Set("Cache-Control", cfg.RestreamCacheControl)
|
|
c.Set("Content-Disposition", `attachment; filename="`+t.Permalink+"."+sc.ToExt(audio)+`"`)
|
|
|
|
if isDownload {
|
|
if t.Artwork != "" {
|
|
t.Artwork = strings.Replace(t.Artwork, "t500x500", "original", 1)
|
|
}
|
|
|
|
switch audio {
|
|
case cfg.AudioMP3:
|
|
req := fasthttp.AcquireRequest()
|
|
resp := fasthttp.AcquireResponse()
|
|
|
|
req.Header.SetUserAgent(cfg.UserAgent)
|
|
|
|
tag := id3v2.NewEmptyTag()
|
|
|
|
tag.SetArtist(t.Author.Username)
|
|
if t.Genre != "" {
|
|
tag.SetGenre(t.Genre)
|
|
}
|
|
|
|
tag.SetTitle(t.Title)
|
|
|
|
if t.Artwork != "" {
|
|
req.SetRequestURI(t.Artwork)
|
|
|
|
err := sc.DoWithRetry(image_httpc, req, resp)
|
|
if err == nil && resp.StatusCode() == 200 {
|
|
//fmt.Println(string(resp.Header.ContentType()), string(resp.Header.Peek("Content-Encoding")), len(resp.Body()))
|
|
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: cfg.B2s(resp.Header.ContentType()), Picture: resp.Body(), PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
|
|
}
|
|
}
|
|
|
|
if tr.Format.Protocol == sc.ProtocolProgressive {
|
|
r := acquireInjector()
|
|
tag.WriteTo(r) // write out tag first because the buffers will be overwritten if you reuse the req/resp
|
|
|
|
req.SetRequestURI(u)
|
|
// enforce streaming here!!
|
|
err := sc.DoWithRetry(misc.HlsStreamingOnlyClient, req, resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.reader = resp.BodyStream()
|
|
r.resp = resp
|
|
return c.SendStream(r)
|
|
}
|
|
|
|
r := acquireReader()
|
|
tag.WriteTo(r)
|
|
r.req = req
|
|
r.resp = resp
|
|
err := r.Setup(u, false, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.SendStream(r)
|
|
case cfg.AudioOpus:
|
|
r := acquireReader()
|
|
err := r.Setup(u, false, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.req.SetRequestURIBytes(r.parts[0])
|
|
err = sc.DoWithRetry(r.client, r.req, r.resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.index++
|
|
res := r.resp.Body()
|
|
// purpose is to inject OpusTags metadata into ogg files
|
|
const until_hdr = len("OggS") + 1 /* ver */ + 0
|
|
const until_seq = until_hdr + 1 /* hdr */ + 8 /* granule */ + 4 /* bitstream */ + 0
|
|
const until_checksum = until_seq + 4 /* seq */ + 0
|
|
const until_segments = until_checksum + 4 /* checksum */ + 1 /* segments num */ + 0
|
|
|
|
// this expects first page to only have 1 segment
|
|
second_page := until_segments + 1 + int(res[until_segments])
|
|
|
|
r.leftover = append(r.leftover, res[:second_page]...)
|
|
|
|
const opustags_prelude = "OpusTags\x04\x00\x00\x00maid" // "OpusTags", uint32 for length, then <length> bytes for vendor string
|
|
const artist = "ARTIST="
|
|
const title = "TITLE="
|
|
const genre = "GENRE="
|
|
// we need to put actual content somewhere else to segment it properly
|
|
var leftover []byte
|
|
{
|
|
ln := len(opustags_prelude) + // opustags hdr
|
|
4 + // number of fields
|
|
4 + len(artist) + len(t.Author.Username) + // ARTIST=...
|
|
4 + len(title) + len(t.Title) // TITLE=...
|
|
|
|
if t.Genre != "" {
|
|
ln += 4 + len(genre) + len(t.Genre)
|
|
}
|
|
leftover = make([]byte, 0, ln)
|
|
}
|
|
|
|
// here come the metadata
|
|
leftover = append(leftover, opustags_prelude...)
|
|
// number of fields
|
|
const default_num = 2
|
|
num := uint32(default_num)
|
|
leftover = append(leftover, default_num, 0, 0, 0)
|
|
// each field in the format of SOME_KEY=SOME_VALUE
|
|
// same approach here, first the field length, then the field itself
|
|
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(artist)+len(t.Author.Username)))
|
|
leftover = append(leftover, artist...)
|
|
leftover = append(leftover, t.Author.Username...)
|
|
|
|
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(title)+len(t.Title)))
|
|
leftover = append(leftover, title...)
|
|
leftover = append(leftover, t.Title...)
|
|
if t.Genre != "" {
|
|
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(genre)+len(t.Genre)))
|
|
leftover = append(leftover, genre...)
|
|
leftover = append(leftover, t.Genre...)
|
|
num++
|
|
}
|
|
|
|
if t.Artwork != "" {
|
|
r.req.SetRequestURI(t.Artwork)
|
|
|
|
err := sc.DoWithRetry(image_httpc, r.req, r.resp)
|
|
if err == nil && r.resp.StatusCode() == 200 {
|
|
// METADATA_BLOCK_PICTURE comes from the FLAC format (https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8), but base64 encoded
|
|
const picture = "METADATA_BLOCK_PICTURE="
|
|
|
|
const emptyshits2 = "\x00\x00\x00\x00" + // desc length
|
|
"\x00\x00\x00\x00" + // width
|
|
"\x00\x00\x00\x00" + // height
|
|
"\x00\x00\x00\x00" + // color depth
|
|
"\x00\x00\x00\x00" // indexed color count
|
|
pic := make([]byte, 0, 4+ // picture type
|
|
4+ // mime len
|
|
len(r.resp.Header.ContentType())+ // mime
|
|
len(emptyshits2)+ // blah blah look above
|
|
4+ // body len
|
|
len(r.resp.Body()))
|
|
pic = append(pic, 0, 0, 0, 3) // picture type (3, Front cover)
|
|
pic = binary.BigEndian.AppendUint32(pic, uint32(len(r.resp.Header.ContentType())))
|
|
pic = append(pic, r.resp.Header.ContentType()...)
|
|
pic = pic[:len(pic)+len(emptyshits2)]
|
|
pic = binary.BigEndian.AppendUint32(pic, uint32(len(r.resp.Body())))
|
|
pic = append(pic, r.resp.Body()...)
|
|
|
|
newLen := base64.StdEncoding.EncodedLen(len(pic))
|
|
// alloc for the picture
|
|
if n := 4 + len(picture) + newLen - (cap(leftover) - len(leftover)); n > 0 {
|
|
leftover = append(leftover[:cap(leftover)], make([]byte, n)...)[:len(leftover)]
|
|
}
|
|
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(picture)+newLen))
|
|
leftover = append(leftover, picture...)
|
|
base64.StdEncoding.Encode(leftover[len(leftover):len(leftover)+newLen], pic)
|
|
leftover = leftover[:len(leftover)+newLen]
|
|
num++
|
|
}
|
|
}
|
|
|
|
if num != default_num {
|
|
binary.LittleEndian.PutUint32(leftover[len(opustags_prelude):], num)
|
|
}
|
|
|
|
const max_possible_page = 255 * 255 // 255 segments, each can have 255 bytes
|
|
if len(leftover) <= max_possible_page { // happy path :) it all fits in one page
|
|
// allocate hdr
|
|
if n := until_segments - (cap(r.leftover) - len(r.leftover)); n > 0 {
|
|
r.leftover = append(r.leftover[:cap(r.leftover)], make([]byte, n)...)[:len(r.leftover)]
|
|
}
|
|
|
|
ptr := len(r.leftover)
|
|
r.leftover = append(r.leftover, res[second_page:second_page+until_checksum]...)
|
|
r.leftover = append(r.leftover, 0, 0, 0, 0) // checksum, to be filled in
|
|
|
|
// lets segment it using ceil division
|
|
segments_num := (len(leftover) + 254) / 255
|
|
r.leftover = append(r.leftover, byte(segments_num))
|
|
// grow the slice before we add allat
|
|
if n := segments_num - (cap(r.leftover) - len(r.leftover)); n > 0 {
|
|
r.leftover = append(r.leftover[:cap(r.leftover)], make([]byte, n)...)[:len(r.leftover)+segments_num]
|
|
}
|
|
|
|
for i := range segments_num - 1 {
|
|
r.leftover[len(r.leftover)-i-2] = 255
|
|
}
|
|
|
|
if n := byte(len(leftover) % 255); n != 0 {
|
|
r.leftover[len(r.leftover)-1] = n
|
|
} else {
|
|
r.leftover[len(r.leftover)-1] = 255
|
|
}
|
|
|
|
r.leftover = append(r.leftover, leftover...)
|
|
|
|
// checksum is calculated for entire page including header
|
|
var crc uint32
|
|
for _, b := range r.leftover[ptr:] {
|
|
crc = (crc << 8) ^ crcTable[(crc>>24)^uint32(b)]
|
|
}
|
|
|
|
binary.LittleEndian.PutUint32(r.leftover[ptr+until_checksum:], crc)
|
|
} else { // sad path :(
|
|
pages_num := (len(leftover) + max_possible_page - 1) / max_possible_page
|
|
|
|
// allocate exactly as much as we need
|
|
if n :=
|
|
pages_num*until_segments + // headers
|
|
((len(leftover) + 254) / 255) + // needed segments
|
|
len(leftover) - // data itself
|
|
|
|
(cap(r.leftover) - len(r.leftover)); n > 0 {
|
|
r.leftover = append(r.leftover[:cap(r.leftover)], make([]byte, n)...)[:len(r.leftover)]
|
|
}
|
|
for i := range pages_num {
|
|
ptr := len(r.leftover)
|
|
r.leftover = append(r.leftover, res[second_page:second_page+until_checksum]...)
|
|
binary.LittleEndian.PutUint32(r.leftover[ptr+until_seq:], uint32(i))
|
|
r.leftover = append(r.leftover, 0, 0, 0, 0) // checksum, to be filled in
|
|
|
|
const (
|
|
Continuation byte = 0x01
|
|
BOS byte = 0x02
|
|
EOS byte = 0x04
|
|
)
|
|
var segments_num int
|
|
var sl []byte
|
|
if i+1 == pages_num {
|
|
r.leftover[ptr+until_hdr] = EOS
|
|
sl = leftover[i*max_possible_page:]
|
|
segments_num = (len(sl) + 254) / 255
|
|
} else {
|
|
if i != 0 {
|
|
r.leftover[ptr+until_hdr] = Continuation
|
|
} else {
|
|
r.leftover[ptr+until_hdr] = BOS
|
|
}
|
|
segments_num = 255
|
|
sl = leftover[i*max_possible_page : (i+1)*max_possible_page]
|
|
}
|
|
r.leftover = append(r.leftover, byte(segments_num))
|
|
|
|
r.leftover = r.leftover[:len(r.leftover)+segments_num]
|
|
for i := range segments_num - 1 {
|
|
r.leftover[len(r.leftover)-i-2] = 255
|
|
}
|
|
|
|
if n := byte(len(sl) % 255); n != 0 {
|
|
r.leftover[len(r.leftover)-1] = n
|
|
} else {
|
|
r.leftover[len(r.leftover)-1] = 255
|
|
}
|
|
|
|
r.leftover = append(r.leftover, sl...)
|
|
|
|
// checksum is calculated for entire page including header
|
|
var crc uint32
|
|
for _, b := range r.leftover[ptr:] {
|
|
crc = (crc << 8) ^ crcTable[(crc>>24)^uint32(b)]
|
|
}
|
|
|
|
binary.LittleEndian.PutUint32(r.leftover[ptr+until_checksum:], crc)
|
|
}
|
|
}
|
|
// dump the rest after original 2nd page
|
|
r.leftover = append(r.leftover, res[second_page+until_segments+int(res[second_page+until_segments])])
|
|
return c.SendStream(r)
|
|
case cfg.AudioAAC:
|
|
r := acquireReader()
|
|
err := r.Setup(u, true, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.req.SetRequestURIBytes(r.parts[0])
|
|
err = sc.DoWithRetry(r.client, r.req, r.resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.index++
|
|
tag, err := mp4meta.ReadMP4(bytes.NewReader(r.resp.Body()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tag.SetArtist(t.Author.Username)
|
|
if t.Genre != "" {
|
|
tag.SetGenre(t.Genre)
|
|
}
|
|
|
|
tag.SetTitle(t.Title)
|
|
|
|
if t.Artwork != "" {
|
|
r.req.SetRequestURI(t.Artwork)
|
|
|
|
err := sc.DoWithRetry(image_httpc, r.req, r.resp)
|
|
if err == nil && r.resp.StatusCode() == 200 {
|
|
parsed, _, err := image.Decode(r.resp.BodyStream())
|
|
r.resp.CloseBodyStream()
|
|
if err == nil {
|
|
tag.SetCoverArt(&parsed)
|
|
}
|
|
}
|
|
}
|
|
|
|
tag.Save(r)
|
|
fixDuration(r.leftover, &t.Duration)
|
|
|
|
return c.SendStream(r)
|
|
}
|
|
}
|
|
|
|
// just the audio file itself, means less processing overhead for us :)
|
|
if tr.Format.Protocol == sc.ProtocolProgressive {
|
|
misc.Log("use progressive")
|
|
req := fasthttp.AcquireRequest()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
|
|
resp := fasthttp.AcquireResponse()
|
|
|
|
req.SetRequestURI(u)
|
|
req.Header.SetUserAgent(cfg.UserAgent)
|
|
|
|
// enforce streaming here!!
|
|
err := sc.DoWithRetry(misc.HlsStreamingOnlyClient, req, resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r := misc.AcquireProxyReader()
|
|
r.Reader = resp.BodyStream()
|
|
r.Resp = resp
|
|
return c.SendStream(r)
|
|
}
|
|
|
|
r := acquireReader()
|
|
if audio == cfg.AudioAAC {
|
|
err = r.Setup(u, true, &t.Duration)
|
|
} else {
|
|
err = r.Setup(u, false, nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.SendStream(r)
|
|
})
|
|
}
|