support many audio presets; fixes; autoplay available (with js) on restream player

This commit is contained in:
Laptop 2024-12-13 23:06:13 +02:00
parent bcb9a4ccc7
commit 31595c9b34
17 changed files with 809 additions and 180 deletions

View file

@ -21,24 +21,31 @@ wip alternative frontend for soundcloud
The UI isn't really done yet. All parameters other than url are unsupported. You can also specify track without the `soundcloud.com` part: `https://sc.maid.zone/w/player/?url=<id>` or `https://sc.maid.zone/w/player/?url=<user>/<track>`
# Viewing instance settings
# Settings
## Viewing instance config
If the instance isn't outdated and has `InstanceInfo` setting enabled, you can navigate to `<instance>/_/info` to view useful instance settings. ([sc.maid.zone/_/info](https://sc.maid.zone/_/info) for example)
An easier way is to navigate to `<instance>/_/preferences`.
## Preferences
If some features are disabled by the instance, they won't show up there.
You can go to `/_/preferences` page to configure them. You can view default preferences using the method described above or by resetting your preferences.
Available features:
You can also Export/Import your preferences, for backup purposes or for easily transfering them between instances.
- Parse descriptions: Highlight `@username`, `https://example.com` and `email@example.com` in text as clickable links
- Show current audio: shows what [preset](#audio-presets) is currently playing (mpeg, opus or aac)
- Proxy images: Retrieve images through the instance, instead of going to soundcloud's servers for them
- Player: In what way should the track be streamed. Can be Restream (does not require JS, better compatibility, can be a bit buggy client-side) or HLS (requires JS, more stable, less good compatibility (you'll be ok unless you are using a very outdated browser))
- Player-specific settings: They will only show up if you have selected HLS player currently.
- - Proxy streams: Retrieve song pieces through the instance, instead of going to soundcloud's servers for them
- - Fully preload track: Fully loads the track when you load the page instead of buffering a small part of it
- - Autoplay next track in playlists: self-explanatory
- - Default autoplay mode: Default mode for autoplaying. Can be normal (play songs in order) or random (play random song)
- Autoplay next track in playlists: self-explanatory
- Default autoplay mode: Default mode for autoplaying. Can be normal (play songs in order) or random (play random song)
- Player-specific settings:
- - HLS Player:
- - - Proxy streams: Retrieve song pieces through the instance, instead of going to soundcloud's servers for them
- - - Fully preload track: Fully loads the track when you load the page instead of buffering a small part of it
- - - Streaming audio: What [preset](#audio-presets) of audio should be streamed (Opus is not supported here)
- - Restream Player:
- - - Streaming audio: What [preset](#audio-presets) of audio should be streamed
# Contributing
Contributions are appreciated! This includes feedback, feature requests, issues, pull requests and etc.
@ -91,6 +98,7 @@ go install github.com/a-h/templ/cmd/templ@latest
5. Download regexp2cg:
Not really required, but helps speed up some parts of the code that use regular expressions. Keep in mind that the `build` script expects this to be installed.
```sh
go install github.com/dlclark/regexp2cg@main
```
@ -108,6 +116,7 @@ Refer to [Configuration guide](#configuration-guide) for configuration informati
7. Build binary:
This uses the `build` script, which generates code from templates, generates code for regular expiressions, and then builds the binary.
```sh
./build
```
@ -187,9 +196,9 @@ Some notes:
| JSON key | Environment variable | Default value | Description |
| :------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| None | SOUNDCLOAK_CONFIG | soundcloak.json | File to load soundcloak config from. If set to`FROM_ENV`, soundcloak loads the config from environment variables. |
| None | SOUNDCLOAK_CONFIG | soundcloak.json | File to load soundcloak config from. If set to `FROM_ENV`, soundcloak loads the config from environment variables. |
| GetWebProfiles | GET_WEB_PROFILES | true | Retrieve links users set in their profile (social media, website, etc) |
| DefaultPreferences | DEFAULT_PREFERENCES | {"Player": "hls", "ProxyStreams": false, "FullyPreloadTrack": false, "ProxyImages": false, "ParseDescriptions": true, "AutoplayNextTrack": false, "DefaultAutoplayMode": "normal"} | see /_/preferences page, default values adapt to your config (Player: "restream" if Restream, else "hls", ProxyStreams and ProxyImages will be same as respective config values) |
| DefaultPreferences | DEFAULT_PREFERENCES | {"Player": "hls", "ProxyStreams": false, "FullyPreloadTrack": false, "ProxyImages": false, "ParseDescriptions": true, "AutoplayNextTrack": false, "DefaultAutoplayMode": "normal", "HLSAudio": "mpeg", "RestreamAudio": "mpeg", "DownloadAudio": "mpeg"} | see /_/preferences page. [Read more](#preferences) |
| ProxyImages | PROXY_IMAGES | false | Enables proxying of images (user avatars, track covers etc) |
| ImageCacheControl | IMAGE_CACHE_CONTROL | max-age=600, public, immutable | [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header value for proxied images. Cached for 10 minutes by default. |
| ProxyStreams | PROXY_STREAMS | false | Enables proxying of song parts and hls playlist files |
@ -211,6 +220,42 @@ Some notes:
</details>
## Preferences
<details>
<summary>Click to view</summary>
| Name | Default | Description | Possible values |
| --------------------- | --------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------ |
| Player | "restream" if Restream is enabled in config, otherwise - "hls" | Method used to play the track in the frontend. HLS - requires JavaScript, loads the track in pieces. Restream - works without JavaScript, loads entire track right away. None - don't play the track | "hls", "restream", "none" |
| ProxyStreams | same as your config | Proxy track streams. Refer to configuration guide for more info. Not effective unless ProxyStreams is enabled in your config and you are using HLS player (Restream proxies songs by default) | true, false |
| FullyPreloadTrack | false | Fully load track when the page is loaded. Only effective if you are using HLS player | true, false |
| ParseDescriptions | true | Highlight links, usernames and emails in track/user/playlist descriptions | true, false |
| AutoplayNextTrack | false | Automatically start playlist playback when you open a track from it | true, false |
| DefaultAutoplayMode | "normal" | Default mode for autoplay. Normal - play songs in order. Random - play random song next | "normal", "random" |
| HLSAudio | "mpeg" | What audio preset should be loaded when using HLS player. Note that "opus" is not supported here. [Read more](#audio-presets) | "aac", "mpeg" |
| RestreamAudio | "mpeg" | What audio preset should be loaded when using Restream player. [Read more](#audio-presets) | "best", "aac", "opus", "mpeg" |
| DownloadAudio | "mpeg" | What audio preset should be loaded when downloading audio with metadata. [Read more](#audio-presets) | "best", "aac", "opus", "mpeg" |
| ShowAudio | false | Show what audio preset was loaded on the track page | true, false |
</details>
## Audio presets
<details>
<summary>Click to view</summary>
| Name | Container | Codec | Bitrate | Note |
| ---- | ---------- | ----- | ------- | -------------------------------------------------------------------------------------- |
| Best | | | | Prefer AAC over Opus over MPEG. Not supported for HLS player (use AAC for same effect) |
| AAC | mp4 (m4a) | AAC | 160kbps | Rarely available. Falls back to MPEG if unavailable |
| Opus | ogg | Opus | 72kbps | Usually available. Falls back to MPEG if unavailable. Not supported for HLS player |
| MP3 | mpeg (mp3) | MP3 | 128kbps | Always available. Good for compatibility |
</details>
# Maintenance-related stuffs
## Updating
@ -263,17 +308,3 @@ npm i
Congratulations! You have succesfully updated your soundcloak.
</details>
# Built with
## Backend
- [Go programming language](https://github.com/golang/go)
- [Fiber (v2)](https://github.com/gofiber/fiber/tree/v2)
- [templ](https://github.com/a-h/templ)
- [fasthttp](https://github.com/valyala/fasthttp)
## Frontend
- HTML, CSS and JavaScript
- [hls.js](https://github.com/video-dev/hls.js)

11
assets/restream.js Normal file
View file

@ -0,0 +1,11 @@
var audio = document.getElementById('track');
var volume = audio.getAttribute('volume');
if (volume) {
audio.volume = parseFloat(volume);
}
var next = audio.getAttribute('data-next');
if (next) {
audio.addEventListener('ended', function() {
location = next + '&volume=' + audio.volume;
});
}

5
go.mod
View file

@ -6,12 +6,16 @@ require (
github.com/a-h/templ v0.2.793
github.com/bogem/id3v2/v2 v2.1.4
github.com/dlclark/regexp2 v1.11.5-0.20240806004527-5bbbed8ea10b
github.com/gcottom/mp4meta v0.0.4
github.com/gcottom/oggmeta v0.0.7
github.com/gofiber/fiber/v2 v2.52.5
github.com/segmentio/encoding v0.4.1
github.com/valyala/fasthttp v1.58.0
)
require (
github.com/abema/go-mp4 v1.2.0 // indirect
github.com/aler9/writerseeker v1.1.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
@ -20,6 +24,7 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sunfish-shogi/bufseekio v0.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect

38
go.sum
View file

@ -1,19 +1,36 @@
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/abema/go-mp4 v1.2.0 h1:gi4X8xg/m179N/J15Fn5ugywN9vtI6PLk6iLldHGLAk=
github.com/abema/go-mp4 v1.2.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/aler9/writerseeker v1.1.0 h1:t+Sm3tjp8scNlqyoa8obpeqwciMNOvdvsxjxEb3Sx3g=
github.com/aler9/writerseeker v1.1.0/go.mod h1:QNCcjSKnLsYoTfMmXkEEfgbz6nNXWxKSaBY+hGJGWDA=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI=
github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5-0.20240806004527-5bbbed8ea10b h1:AJKOdc+1fRSJ0/75Jty1npvxUUD0y7hQDg15LMAHhyU=
github.com/dlclark/regexp2 v1.11.5-0.20240806004527-5bbbed8ea10b/go.mod h1:YvCrhrh/qlds8EhFKPtJprdXn5fWBllSw1qo99dZyiQ=
github.com/gcottom/mp4meta v0.0.4 h1:oHdl5Ad0A0PQCTxthD0KFEQG7pUxG8oOLmTWmgnbncQ=
github.com/gcottom/mp4meta v0.0.4/go.mod h1:qstBsqczbFF1e90C828qv966Xjx/LFPjWQITARCpyfA=
github.com/gcottom/oggmeta v0.0.7 h1:6mKm/9xhDeKKIOrB0K+daJoXOLbyRq84e5xV3dndDt8=
github.com/gcottom/oggmeta v0.0.7/go.mod h1:ifdINhphaEW587BIgA+m5TJwCoVk88JuZMCHwXc/gpM=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -21,6 +38,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -28,6 +49,14 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w=
github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
@ -46,6 +75,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -66,3 +96,11 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -35,6 +35,20 @@ const (
AutoplayRandom string = "random"
)
const (
// choose best for quality/size (AudioAAC over AudioOpus over AudioMP3)
AudioBest string = "best"
// 160kbps m4a AAC audio, rarely available (fallback to AudioMP3 if unavailable)
AudioAAC string = "aac"
// 72kbps ogg opus audio, usually available 99% of the time (fallback to AudioMP3 if unavailable)
AudioOpus string = "opus"
// 128kbps mp3 audio, always available, good for compatibility
AudioMP3 string = "mpeg"
)
type Preferences struct {
Player *string
ProxyStreams *bool
@ -52,7 +66,15 @@ type Preferences struct {
// Automatically play next track in playlists
AutoplayNextTrack *bool
DefaultAutoplayMode *string
DefaultAutoplayMode *string // "normal" or "random"
// Check line 38 for constants
// Probably best to keep all at "mpeg" by default for compatibility
HLSAudio *string // Please don't use "opus" or "best". hls.js doesn't work with ogg/opus
RestreamAudio *string // You can actually use anything here
DownloadAudio *string // "aac" may not play well with some players
ShowAudio *bool // display audio (aac, opus, mpeg etc) under track player
}
// // config // //
@ -156,6 +178,13 @@ func defaultPreferences() {
p2 := AutoplayNormal
DefaultPreferences.DefaultAutoplayMode = &p2
p3 := AudioMP3
DefaultPreferences.HLSAudio = &p3
DefaultPreferences.RestreamAudio = &p3
DefaultPreferences.DownloadAudio = &p3
DefaultPreferences.ShowAudio = &False
}
func loadDefaultPreferences(loaded Preferences) {
@ -207,6 +236,31 @@ func loadDefaultPreferences(loaded Preferences) {
p := AutoplayNormal
DefaultPreferences.DefaultAutoplayMode = &p
}
p := AudioMP3
if loaded.HLSAudio != nil {
DefaultPreferences.HLSAudio = loaded.HLSAudio
} else {
DefaultPreferences.HLSAudio = &p
}
if loaded.RestreamAudio != nil {
DefaultPreferences.RestreamAudio = loaded.RestreamAudio
} else {
DefaultPreferences.RestreamAudio = &p
}
if loaded.DownloadAudio != nil {
DefaultPreferences.DownloadAudio = loaded.DownloadAudio
} else {
DefaultPreferences.DownloadAudio = &p
}
if loaded.ShowAudio != nil {
DefaultPreferences.ShowAudio = loaded.ShowAudio
} else {
DefaultPreferences.ShowAudio = &False
}
}
func boolean(in string) bool {
@ -514,6 +568,8 @@ func init() {
// seems soundcloud has 4 of these (i1, i2, i3, i4)
// they point to the same ip from my observations, and they all serve the same files
const ImageCDN = "i1.sndcdn.com"
const HLSCDN = "cf-hls-media.sndcdn.com"
const HLSAACCDN = "playback.media-streaming.soundcloud.cloud"
var True = true
var False = false

View file

@ -38,6 +38,22 @@ func Defaults(dst *cfg.Preferences) {
if dst.DefaultAutoplayMode == nil {
dst.DefaultAutoplayMode = cfg.DefaultPreferences.DefaultAutoplayMode
}
if dst.HLSAudio == nil {
dst.HLSAudio = cfg.DefaultPreferences.HLSAudio
}
if dst.RestreamAudio == nil {
dst.RestreamAudio = cfg.DefaultPreferences.RestreamAudio
}
if dst.DownloadAudio == nil {
dst.DownloadAudio = cfg.DefaultPreferences.DownloadAudio
}
if dst.ShowAudio == nil {
dst.ShowAudio = cfg.DefaultPreferences.ShowAudio
}
}
func Get(c *fiber.Ctx) (cfg.Preferences, error) {
@ -61,6 +77,10 @@ type PrefsForm struct {
FullyPreloadTrack string
AutoplayNextTrack string
DefaultAutoplayMode string
HLSAudio string
RestreamAudio string
DownloadAudio string
ShowAudio string
}
type Export struct {
@ -94,19 +114,25 @@ func Load(r fiber.Router) {
old.DefaultAutoplayMode = &p.DefaultAutoplayMode
}
if *old.Player == "hls" {
if p.AutoplayNextTrack == "on" {
old.AutoplayNextTrack = &cfg.True
} else {
old.AutoplayNextTrack = &cfg.False
}
if p.ShowAudio == "on" {
old.ShowAudio = &cfg.True
} else {
old.ShowAudio = &cfg.False
}
if *old.Player == cfg.HLSPlayer {
if cfg.ProxyStreams {
if p.ProxyStreams == "on" {
old.ProxyStreams = &cfg.ProxyStreams // true!
} else if p.ProxyStreams == "" {
old.ProxyStreams = &cfg.False
}
if p.AutoplayNextTrack == "on" {
old.AutoplayNextTrack = &cfg.True
} else {
old.AutoplayNextTrack = &cfg.False
}
}
if p.FullyPreloadTrack == "on" {
@ -114,6 +140,16 @@ func Load(r fiber.Router) {
} else if p.FullyPreloadTrack == "" {
old.FullyPreloadTrack = &cfg.False
}
old.HLSAudio = &p.HLSAudio
}
if cfg.Restream {
if *old.Player == cfg.RestreamPlayer {
old.RestreamAudio = &p.RestreamAudio
}
old.DownloadAudio = &p.DownloadAudio
}
if cfg.ProxyImages {

View file

@ -9,7 +9,18 @@ import (
"github.com/valyala/fasthttp"
)
var httpc *fasthttp.HostClient
func Load(r fiber.Router) {
httpc = &fasthttp.HostClient{
Addr: cfg.ImageCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
StreamResponseBody: true,
}
r.Get("/_/proxy/images", func(c *fiber.Ctx) error {
url := c.Query("url")
if url == "" {
@ -40,7 +51,7 @@ func Load(r fiber.Router) {
resp := fasthttp.AcquireResponse()
//defer fasthttp.ReleaseResponse(resp) moved to proxyreader!!!
err = sc.DoWithRetry(sc.ImageClient, req, resp)
err = sc.DoWithRetry(httpc, req, resp)
if err != nil {
return err
}

View file

@ -10,18 +10,28 @@ import (
"github.com/valyala/fasthttp"
)
const cdn = "cf-hls-media.sndcdn.com"
var httpc = &fasthttp.HostClient{
Addr: cdn + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
StreamResponseBody: true,
}
var httpc *fasthttp.HostClient
var httpc_aac *fasthttp.HostClient
func Load(r fiber.Router) {
httpc = &fasthttp.HostClient{
Addr: cfg.HLSCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
StreamResponseBody: true,
}
httpc_aac = &fasthttp.HostClient{
Addr: cfg.HLSAACCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
StreamResponseBody: true,
}
r.Get("/_/proxy/streams", func(c *fiber.Ctx) error {
ur := c.Query("url")
if ur == "" {
@ -36,7 +46,7 @@ func Load(r fiber.Router) {
return err
}
if !bytes.Equal(parsed.Host(), []byte(cdn)) {
if !bytes.HasSuffix(parsed.Host(), []byte(".sndcdn.com")) {
return fiber.ErrBadRequest
}
@ -61,6 +71,44 @@ func Load(r fiber.Router) {
return c.SendStream(pr)
})
r.Get("/_/proxy/streams/aac", func(c *fiber.Ctx) error {
ur := c.Query("url")
if ur == "" {
return fiber.ErrBadRequest
}
parsed := fasthttp.AcquireURI()
defer fasthttp.ReleaseURI(parsed)
err := parsed.Parse(nil, []byte(ur))
if err != nil {
return err
}
if !bytes.HasSuffix(parsed.Host(), []byte(".soundcloud.cloud")) {
return fiber.ErrBadRequest
}
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetURI(parsed)
req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse()
err = sc.DoWithRetry(httpc_aac, req, resp)
if err != nil {
return err
}
pr := cfg.AcquireProxyReader()
pr.Reader = resp.BodyStream()
pr.Resp = resp
return c.SendStream(pr)
})
r.Get("/_/proxy/streams/playlist", func(c *fiber.Ctx) error {
ur := c.Query("url")
if ur == "" {
@ -75,7 +123,7 @@ func Load(r fiber.Router) {
return err
}
if !bytes.Equal(parsed.Host(), []byte(cdn)) {
if !bytes.HasSuffix(parsed.Host(), []byte(".sndcdn.com")) {
return fiber.ErrBadRequest
}
@ -111,4 +159,64 @@ func Load(r fiber.Router) {
return c.Send(bytes.Join(sp, []byte("\n")))
})
r.Get("/_/proxy/streams/playlist/aac", func(c *fiber.Ctx) error {
ur := c.Query("url")
if ur == "" {
return fiber.ErrBadRequest
}
parsed := fasthttp.AcquireURI()
defer fasthttp.ReleaseURI(parsed)
err := parsed.Parse(nil, []byte(ur))
if err != nil {
return err
}
if !bytes.HasSuffix(parsed.Host(), []byte(".soundcloud.cloud")) {
return fiber.ErrBadRequest
}
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetURI(parsed)
req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = sc.DoWithRetry(httpc_aac, req, resp)
if err != nil {
return err
}
data, err := resp.BodyUncompressed()
if err != nil {
data = resp.Body()
}
var sp = bytes.Split(data, []byte("\n"))
for i, l := range sp {
if len(l) == 0 {
continue
}
if l[0] == '#' {
if bytes.HasPrefix(l, []byte(`#EXT-X-MAP:URI="`)) {
l = []byte(`#EXT-X-MAP:URI="/_/proxy/streams/aac?url=` + url.QueryEscape(string(l[16:len(l)-1])) + `"`)
sp[i] = l
}
continue
}
l = []byte("/_/proxy/streams/aac?url=" + url.QueryEscape(string(l)))
sp[i] = l
}
return c.Send(bytes.Join(sp, []byte("\n")))
})
}

View file

@ -2,49 +2,40 @@ package restream
import (
"bytes"
"image/jpeg"
"io"
"github.com/bogem/id3v2/v2"
"github.com/gcottom/mp4meta"
"github.com/gcottom/oggmeta"
"github.com/gofiber/fiber/v2"
"github.com/maid-zone/soundcloak/lib/cfg"
"github.com/maid-zone/soundcloak/lib/preferences"
"github.com/maid-zone/soundcloak/lib/sc"
"github.com/valyala/fasthttp"
)
const cdn = "cf-hls-media.sndcdn.com"
var httpc = &fasthttp.HostClient{
Addr: cdn + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
}
// Needed for restream to work even if prefs.Player != RestreamPlayer
var stubPrefs = cfg.Preferences{}
func init() {
p := cfg.RestreamPlayer
stubPrefs.Player = &p
f := false
stubPrefs.ProxyStreams = &f
stubPrefs.ProxyImages = &f
}
var httpc *fasthttp.HostClient
var httpc_aac *fasthttp.HostClient
var httpc_image *fasthttp.HostClient
type reader struct {
parts [][]byte
leftover []byte
index int
req *fasthttp.Request
resp *fasthttp.Response
req *fasthttp.Request
resp *fasthttp.Response
client *fasthttp.HostClient
}
func (r *reader) Setup(url string) error {
func clone(buf []byte) []byte {
out := make([]byte, len(buf))
copy(out, buf)
return out
}
func (r *reader) Setup(url string, aac bool) error {
r.req = fasthttp.AcquireRequest()
r.resp = fasthttp.AcquireResponse()
@ -52,7 +43,13 @@ func (r *reader) Setup(url string) error {
r.req.Header.Set("User-Agent", cfg.UserAgent)
r.req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
err := httpc.Do(r.req, r.resp)
if aac {
r.client = httpc_aac
} else {
r.client = httpc
}
err := sc.DoWithRetry(r.client, r.req, r.resp)
if err != nil {
return err
}
@ -62,12 +59,31 @@ func (r *reader) Setup(url string) error {
data = r.resp.Body()
}
for _, s := range bytes.Split(data, []byte{'\n'}) {
if len(s) == 0 || s[0] == '#' {
continue
}
r.parts = make([][]byte, 0, 16)
if aac {
// clone needed to mitigate memory skill issues here
for _, s := range bytes.Split(data, []byte{'\n'}) {
if len(s) == 0 {
continue
}
if s[0] == '#' {
if bytes.HasPrefix(s, []byte(`#EXT-X-MAP:URI="`)) {
r.parts = append(r.parts, clone(s[16:len(s)-1]))
}
r.parts = append(r.parts, s)
continue
}
r.parts = append(r.parts, clone(s))
}
} else {
for _, s := range bytes.Split(data, []byte{'\n'}) {
if len(s) == 0 || s[0] == '#' {
continue
}
r.parts = append(r.parts, s)
}
}
return nil
@ -105,7 +121,7 @@ func (r *reader) Read(buf []byte) (n int, err error) {
r.req.SetRequestURIBytes(r.parts[r.index])
err = httpc.Do(r.req, r.resp)
err = sc.DoWithRetry(r.client, r.req, r.resp)
if err != nil {
return
}
@ -115,7 +131,11 @@ func (r *reader) Read(buf []byte) (n int, err error) {
data = r.resp.Body()
}
n = copy(buf, data[:len(buf)])
if len(data) > len(buf) {
n = copy(buf, data[:len(buf)])
} else {
n = copy(buf, data)
}
r.leftover = data[n:]
r.index++
@ -137,7 +157,39 @@ func (c *collector) Write(data []byte) (n int, err error) {
}
func Load(r fiber.Router) {
httpc = &fasthttp.HostClient{
Addr: cfg.HLSCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
}
httpc_aac = &fasthttp.HostClient{
Addr: cfg.HLSAACCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
}
httpc_image = &fasthttp.HostClient{
Addr: cfg.ImageCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
StreamResponseBody: true,
}
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
@ -148,46 +200,231 @@ func Load(r fiber.Router) {
return err
}
tr := t.Media.SelectCompatible()
var isDownload = c.Query("metadata") == "true"
var quality *string
if isDownload {
quality = p.DownloadAudio
} else {
quality = p.RestreamAudio
}
tr, audio := t.Media.SelectCompatible(*quality, true)
if tr == nil {
return fiber.ErrExpectationFailed
}
u, err := tr.GetStream(cid, stubPrefs, t.Authorization)
u, err := tr.GetStream(cid, p, t.Authorization)
if err != nil {
return err
}
c.Set("Content-Type", "audio/mpeg")
c.Set("Content-Type", tr.Format.MimeType)
c.Set("Cache-Control", cfg.RestreamCacheControl)
r := reader{}
if err := r.Setup(u); err != nil {
return err
}
if isDownload {
switch audio {
case cfg.AudioMP3:
r := reader{}
if err := r.Setup(u, false); err != nil {
return err
}
if c.Query("metadata") == "true" {
tag := id3v2.NewEmptyTag()
tag := id3v2.NewEmptyTag()
tag.SetArtist(t.Author.Username)
if t.Genre != "" {
tag.SetGenre(t.Genre)
}
tag.SetArtist(t.Author.Username)
if t.Genre != "" {
tag.SetGenre(t.Genre)
}
tag.SetTitle(t.Title)
tag.SetTitle(t.Title)
if t.Artwork != "" {
data, mime, err := t.DownloadImage()
if t.Artwork != "" {
data, mime, err := t.DownloadImage()
if err != nil {
return err
}
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: mime, Picture: data, PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
}
var col collector
tag.WriteTo(&col)
r.leftover = col.data
// id3 is quite flexible and the files streamed by soundcloud don't have it so its easy to restream the stuff like this
return c.SendStream(&r)
case cfg.AudioOpus:
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetRequestURI(u)
req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = sc.DoWithRetry(httpc, req, resp)
if err != nil {
return err
}
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: mime, Picture: data, PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
}
data, err := resp.BodyUncompressed()
if err != nil {
data = resp.Body()
}
var c collector
tag.WriteTo(&c)
r.leftover = c.data
parts := make([][]byte, 0, 16)
for _, s := range bytes.Split(data, []byte{'\n'}) {
if len(s) == 0 || s[0] == '#' {
continue
}
parts = append(parts, s)
}
result := []byte{}
for _, part := range parts {
req.SetRequestURIBytes(part)
err = sc.DoWithRetry(httpc, req, resp)
if err != nil {
return err
}
data, err = resp.BodyUncompressed()
if err != nil {
data = resp.Body()
}
result = append(result, data...)
}
tag, err := oggmeta.ReadOGG(bytes.NewReader(result))
if err != nil {
return err
}
tag.SetArtist(t.Author.Username)
if t.Genre != "" {
tag.SetGenre(t.Genre)
}
tag.SetTitle(t.Title)
if t.Artwork != "" {
req.SetRequestURI(t.Artwork)
req.Header.Del("Accept-Encoding")
err := sc.DoWithRetry(httpc_image, req, resp)
if err != nil {
return err
}
defer resp.CloseBodyStream()
parsed, err := jpeg.Decode(resp.BodyStream())
if err != nil {
return err
}
tag.SetCoverArt(&parsed)
}
return tag.Save(c)
case cfg.AudioAAC:
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetRequestURI(u)
req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = sc.DoWithRetry(httpc_aac, req, resp)
if err != nil {
return err
}
data, err := resp.BodyUncompressed()
if err != nil {
data = resp.Body()
}
parts := make([][]byte, 0, 16)
// clone needed to mitigate memory skill issues here
for _, s := range bytes.Split(data, []byte{'\n'}) {
if len(s) == 0 {
continue
}
if s[0] == '#' {
if bytes.HasPrefix(s, []byte(`#EXT-X-MAP:URI="`)) {
parts = append(parts, clone(s[16:len(s)-1]))
}
continue
}
parts = append(parts, clone(s))
}
result := []byte{}
for _, part := range parts {
req.SetRequestURIBytes(part)
err = sc.DoWithRetry(httpc_aac, req, resp)
if err != nil {
return err
}
data, err = resp.BodyUncompressed()
if err != nil {
data = resp.Body()
}
result = append(result, data...)
}
tag, err := mp4meta.ReadMP4(bytes.NewReader(result))
if err != nil {
return err
}
tag.SetArtist(t.Author.Username)
if t.Genre != "" {
tag.SetGenre(t.Genre)
}
tag.SetTitle(t.Title)
if t.Artwork != "" {
req.SetRequestURI(t.Artwork)
req.Header.Del("Accept-Encoding")
err := sc.DoWithRetry(httpc_image, req, resp)
if err != nil {
return err
}
defer resp.CloseBodyStream()
parsed, err := jpeg.Decode(resp.BodyStream())
if err != nil {
return err
}
tag.SetCoverArt(&parsed)
}
return tag.Save(c)
}
}
r := reader{}
if err := r.Setup(u, audio == cfg.AudioAAC); err != nil {
return err
}
return c.SendStream(&r)

View file

@ -33,7 +33,7 @@ var httpc = &fasthttp.HostClient{
MaxIdleConnDuration: 1<<63 - 1,
}
var ImageClient = &fasthttp.HostClient{
var httpc_image = &fasthttp.HostClient{
Addr: cfg.ImageCDN + ":443",
IsTLS: true,
DialDualStack: true,
@ -121,7 +121,7 @@ func processFile(wg *sync.WaitGroup, ch chan string, uri string, isDone *bool) {
}
// Experimental method, which asserts that the clientId is inside the file that starts with "0-"
const experimental_GetClientID = false
const experimental_GetClientID = true
// inspired by github.com/imputnet/cobalt (mostly stolen lol)
func GetClientID() (string, error) {
@ -272,6 +272,8 @@ func DoWithRetry(httpc *fasthttp.HostClient, req *fasthttp.Request, resp *fastht
err.Error() != "timeout" {
return
}
cfg.Log("we failed haha", err)
}
return
@ -398,6 +400,10 @@ func TagListParser(taglist string) (res []string) {
cur = append(cur, c)
}
if len(cur) != 0 {
res = append(res, string(cur))
}
return
}

View file

@ -80,14 +80,42 @@ type Stream struct {
URL string `json:"url"`
}
func (m Media) SelectCompatible() *Transcoding {
for _, t := range m.Transcodings {
if t.Format.Protocol == ProtocolHLS && t.Format.MimeType == "audio/mpeg" {
return &t
func (m Media) SelectCompatible(mode string, opus bool) (*Transcoding, string) {
switch mode {
case cfg.AudioBest:
for _, t := range m.Transcodings {
if t.Format.Protocol == ProtocolHLS && t.Preset == "aac_160k" {
return &t, cfg.AudioAAC
}
}
if opus {
for _, t := range m.Transcodings {
if t.Format.Protocol == ProtocolHLS && strings.HasPrefix(t.Preset, "opus_") {
return &t, cfg.AudioOpus
}
}
}
case cfg.AudioAAC:
for _, t := range m.Transcodings {
if t.Format.Protocol == ProtocolHLS && t.Preset == "aac_160k" {
return &t, cfg.AudioAAC
}
}
case cfg.AudioOpus:
for _, t := range m.Transcodings {
if t.Format.Protocol == ProtocolHLS && strings.HasPrefix(t.Preset, "opus_") {
return &t, cfg.AudioOpus
}
}
}
return nil
for _, t := range m.Transcodings {
if t.Format.Protocol == ProtocolHLS && t.Format.MimeType == "audio/mpeg" {
return &t, cfg.AudioMP3
}
}
return nil, ""
}
func GetTrack(cid string, permalink string) (Track, error) {
@ -296,11 +324,17 @@ func (tr Transcoding) GetStream(cid string, prefs cfg.Preferences, authorization
return "", err
}
cfg.Log(s)
if s.URL == "" {
return "", ErrNoURL
}
if cfg.ProxyStreams && *prefs.ProxyStreams && *prefs.Player == cfg.HLSPlayer {
if tr.Preset == "aac_160k" {
return "/_/proxy/streams/playlist/aac?url=" + url.QueryEscape(s.URL), nil
}
return "/_/proxy/streams/playlist?url=" + url.QueryEscape(s.URL), nil
}
@ -423,7 +457,7 @@ func (t Track) DownloadImage() ([]byte, string, error) {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err := DoWithRetry(ImageClient, req, resp)
err := DoWithRetry(httpc_image, req, resp)
if err != nil {
return nil, "", err
}

View file

@ -9,13 +9,9 @@ import (
"github.com/dlclark/regexp2"
)
//var wordre = regexp.MustCompile(`\S+`)
// var usernamere = regexp.MustCompile(`@[a-zA-Z0-9\-]+`)
// var urlre = regexp.MustCompile(`https?:\/\/[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{1,6}[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*`)
//go:generate regexp2cg -package textparsing -o regexp2_codegen.go
var emailre = regexp2.MustCompile(`^[-a-zA-Z0-9%._\+~#=]+@[-a-zA-Z0-9%._\+~=&]{2,256}\.[a-z]{1,6}$`, 0)
var theregex = regexp2.MustCompile(`@[a-zA-Z0-9\-]+|(?:https?:\/\/[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{1,6}[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)|(?:[-a-zA-Z0-9%._\+~#=]+@[-a-zA-Z0-9%._\+~=&]{2,256}\.[a-z]{1,6})`, 0)
var theregex = regexp2.MustCompile(`@[a-zA-Z0-9\-_]+|(?:https?:\/\/[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{1,6}[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)|(?:[-a-zA-Z0-9%._\+~#=]+@[-a-zA-Z0-9%._\+~=&]{2,256}\.[a-z]{1,6})`, 0)
func IsEmail(s string) bool {
t, _ := emailre.MatchString(s)

24
main.go
View file

@ -9,6 +9,7 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/segmentio/encoding/json"
"github.com/valyala/fasthttp"
@ -167,7 +168,7 @@ func main() {
stream := ""
if *prefs.Player != cfg.NonePlayer {
tr := track.Media.SelectCompatible()
tr, _ := track.Media.SelectCompatible(*prefs.HLSAudio, false)
if tr == nil {
err = sc.ErrIncompatibleStream
} else if *prefs.Player == cfg.HLSPlayer {
@ -472,13 +473,22 @@ func main() {
displayErr := ""
stream := ""
audio := ""
if *prefs.Player != cfg.NonePlayer {
tr := track.Media.SelectCompatible()
if tr == nil {
err = sc.ErrIncompatibleStream
} else if *prefs.Player == cfg.HLSPlayer {
stream, err = tr.GetStream(cid, prefs, track.Authorization)
if *prefs.Player == cfg.HLSPlayer {
var tr *sc.Transcoding
tr, audio = track.Media.SelectCompatible(*prefs.HLSAudio, false)
if tr == nil {
err = sc.ErrIncompatibleStream
} else {
stream, err = tr.GetStream(cid, prefs, track.Authorization)
}
} else {
_, audio = track.Media.SelectCompatible(*prefs.RestreamAudio, true)
if audio == "" {
err = sc.ErrIncompatibleStream
}
}
if err != nil {
@ -534,7 +544,7 @@ func main() {
}
c.Set("Content-Type", "text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, c.Query("autoplay") == "true", playlist, nextTrack, c.Query("volume"), mode), templates.TrackHeader(prefs, track)).Render(context.Background(), c)
return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, c.Query("autoplay") == "true", playlist, nextTrack, c.Query("volume"), mode, audio), templates.TrackHeader(prefs, track)).Render(context.Background(), c)
})
app.Get("/:user", func(c *fiber.Ctx) error {

View file

@ -25,5 +25,5 @@
"Addr": "127.0.0.1:4664"
// Note: if you are going to make your own config, you should start from scratch, or remove the comments. Comments aren't allowed in JSON, but they are used here to explain some stuff.
// For all the possible configuration keys, refer to lib/cfg/init.go file.
// For more information, refer to README.md (Configuration guide) and lib/cfg/init.go
}

View file

@ -30,6 +30,15 @@ templ sel(name string, options []option, selected string) {
</select>
}
templ sel_audio(name string, selected string, noOpus bool) {
@sel(name, []option{
{cfg.AudioBest, "Best", noOpus},
{cfg.AudioAAC, "M4A AAC 160kb/s", false},
{cfg.AudioOpus, "OGG Opus 72kb/s", noOpus},
{cfg.AudioMP3, "MP3 128kb/s", false},
}, selected)
}
templ Preferences(prefs cfg.Preferences) {
<h1>Preferences</h1>
<form method="post" autocomplete="off">
@ -37,68 +46,82 @@ templ Preferences(prefs cfg.Preferences) {
Parse descriptions:
@checkbox("ParseDescriptions", *prefs.ParseDescriptions)
</label>
<br/>
<label>
Show current audio:
@checkbox("ShowAudio", *prefs.ShowAudio)
</label>
if cfg.ProxyImages {
<label>
Proxy images:
@checkbox("ProxyImages", *prefs.ProxyImages)
</label>
<br/>
}
if cfg.Restream {
<label>
Download audio:
@sel_audio("DownloadAudio", *prefs.DownloadAudio, false)
</label>
}
<label>
Autoplay next track in playlists:
@checkbox("AutoplayNextTrack", *prefs.AutoplayNextTrack)
(requires JS)
</label>
if *prefs.AutoplayNextTrack {
<label>
Default autoplay mode:
@sel("DefaultAutoplayMode", []option{
{"normal", "Normal (play songs in order)", false},
{"random", "Random (play random song)", false},
}, *prefs.DefaultAutoplayMode)
</label>
}
<label>
Player:
@sel("Player", []option{
{"restream", "Restream Player", !cfg.Restream},
{"hls", "HLS Player (more stable, requires JS)", false},
{"none", "None", false},
{cfg.RestreamPlayer, "Restream Player", !cfg.Restream},
{cfg.HLSPlayer, "HLS Player (requires JS)", false},
{cfg.NonePlayer, "None", false},
}, *prefs.Player)
</label>
<br/>
if *prefs.Player == "hls" {
<h1>Player-specific preferences</h1>
if cfg.ProxyStreams {
switch *prefs.Player {
case cfg.HLSPlayer:
<h1>Player-specific preferences</h1>
if cfg.ProxyStreams {
<label>
Proxy song streams:
@checkbox("ProxyStreams", *prefs.ProxyStreams)
</label>
}
<label>
Proxy song streams:
@checkbox("ProxyStreams", *prefs.ProxyStreams)
Fully preload track:
@checkbox("FullyPreloadTrack", *prefs.FullyPreloadTrack)
</label>
<br/>
}
<label>
Fully preload track:
@checkbox("FullyPreloadTrack", *prefs.FullyPreloadTrack)
</label>
<br/>
<label>
Autoplay next track in playlists:
@checkbox("AutoplayNextTrack", *prefs.AutoplayNextTrack)
</label>
if *prefs.AutoplayNextTrack {
<br/>
<label>
Default autoplay mode:
@sel("DefaultAutoplayMode", []option{
{"normal", "Normal (play songs in order)", false},
{"random", "Random (play random song)", false},
}, *prefs.DefaultAutoplayMode)
Streaming audio:
@sel_audio("HLSAudio", *prefs.HLSAudio, true)
</label>
case cfg.RestreamPlayer:
<h1>Player-specific preferences</h1>
<label>
Streaming audio:
@sel_audio("RestreamAudio", *prefs.RestreamAudio, false)
</label>
}
<br/>
}
<input type="submit" value="Update" class="btn" style="margin-top: 1rem;"/>
<br/>
<br/>
<p>These preferences get saved in a cookie.</p>
</form>
<h1>Management</h1>
<h2>Preferences</h2>
<div style="display: flex; gap: 1rem;">
<a class="btn" href="/_/preferences/export" download="soundcloak_preferences.json">Export</a>
<a class="btn" href="/_/preferences/reset">Reset</a>
</div>
<br>
<br/>
<form method="post" action="/_/preferences/import" autocomplete="off" style="display: grid; gap: 1rem;" enctype="multipart/form-data">
<input class="btn" type="file" autocomplete="off" name="prefs" />
<input class="btn" type="file" autocomplete="off" name="prefs"/>
<input type="submit" value="Import" class="btn"/>
</form>
<style>label{display:flex;gap:.5rem;align-items:center;margin-bottom:.35rem}</style>
}

View file

@ -8,6 +8,19 @@ import (
"strings"
)
func toExt(audio string) string {
switch audio {
case cfg.AudioAAC:
return "m4a"
case cfg.AudioOpus:
return "ogg"
case cfg.AudioMP3:
return "mp3"
}
return ""
}
templ TrackHeader(prefs cfg.Preferences, t sc.Track) {
<meta name="og:site_name" content={ t.Author.Username + " ~ soundcloak" }/>
<meta name="og:title" content={ t.Title }/>
@ -33,16 +46,24 @@ func next(t *sc.Track, p *sc.Playlist, autoplay bool, mode string, volume string
return r
}
templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayErr string, autoplay bool, nextTrack *sc.Track, playlist *sc.Playlist, volume string, mode string) {
templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayErr string, autoplay bool, nextTrack *sc.Track, playlist *sc.Playlist, volume string, mode string, audio string) {
if *prefs.Player == cfg.NonePlayer {
{{ return }}
}
if displayErr == "" {
{{ var audioPref string }}
if cfg.Restream && *prefs.Player == cfg.RestreamPlayer {
<audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
} else if stream != "" {
{{ audioPref = *prefs.RestreamAudio }}
if nextTrack != nil {
<audio id="track" src={ stream } controls autoplay?={ autoplay } data-next={next(nextTrack, playlist, true, mode, "")} volume={volume}></audio>
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(nextTrack, playlist, true, mode, "") } volume={volume}></audio>
<script async src="/restream.js"></script>
} else {
<audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
}
} else if stream != "" {
{{ audioPref = *prefs.HLSAudio }}
if nextTrack != nil {
<audio id="track" src={ stream } controls autoplay?={ autoplay } data-next={ next(nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
} else {
<audio id="track" src={ stream } controls autoplay?={ autoplay }></audio>
}
@ -60,6 +81,15 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
}
</noscript>
}
if *prefs.ShowAudio {
<div>
if audioPref == cfg.AudioBest {
<p>Audio: best ({ audio })</p>
} else {
<p>Audio: { audio }</p>
}
</div>
}
} else {
<div>
<p style="white-space: pre-wrap;">{ displayErr }</p>
@ -69,9 +99,11 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
templ TrackItem(track *sc.Track, showUsername bool, overrideHref string) {
if track.Title != "" {
{{ if overrideHref == "" {
overrideHref = track.Href()
} }}
{{
if overrideHref == "" {
overrideHref = track.Href()
}
}}
<a class="listing" href={ templ.URL(overrideHref) }>
if track.Artwork != "" {
<img src={ track.Artwork }/>
@ -88,42 +120,37 @@ templ TrackItem(track *sc.Track, showUsername bool, overrideHref string) {
}
}
templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string, autoplay bool, playlist *sc.Playlist, nextTrack *sc.Track, volume string, mode string) {
templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string, autoplay bool, playlist *sc.Playlist, nextTrack *sc.Track, volume string, mode string, audio string) {
if t.Artwork != "" {
<img src={ t.Artwork } width="300px"/>
}
<h1>{ t.Title }</h1>
@TrackPlayer(prefs, t, stream, displayErr, autoplay, nextTrack, playlist, volume, mode)
@TrackPlayer(prefs, t, stream, displayErr, autoplay, nextTrack, playlist, volume, mode, audio)
if t.Genre != "" {
<p class="tag">{ t.Genre }</p>
} else {
<br/>
<br/>
}
if playlist != nil {
<details open style="margin-bottom: 1rem;">
<summary>Playback info</summary>
<h2>In playlist:</h2>
@PlaylistItem(playlist, true)
<h2>Next track:</h2>
@TrackItem(nextTrack, true, next(nextTrack, playlist, true, mode, volume))
<a href={templ.URL(t.Href())} class="link">Stop playlist playback</a>
<br>
<div style="display: flex; gap: 1rem">
<a href={ templ.URL(t.Href()) } class="btn">Stop playlist playback</a>
if mode != cfg.AutoplayRandom {
<a href={templ.URL(next(&t, playlist, false, cfg.AutoplayRandom, volume))} class="link">Switch to random mode</a>
<a href={ templ.URL(next(&t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
} else {
<a href={templ.URL(next(&t, playlist, false, cfg.AutoplayNormal, volume))} class="link">Switch to normal mode</a>
<a href={ templ.URL(next(&t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
}
</div>
</details>
}
@UserItem(&t.Author)
<div style="display: flex; gap: 1rem">
<a class="btn" href={ templ.URL("https://soundcloud.com" + t.Href()) }>view on soundcloud</a>
if cfg.Restream {
<a class="btn" href={ templ.URL("/_/restream" + t.Href() + "?metadata=true") } download={ t.Author.Username + " - " + t.Title + ".mp3" }>download</a>
<a class="btn" href={ templ.URL("/_/restream" + t.Href() + "?metadata=true") } download={t.Permalink + "." + toExt(audio)}>download</a>
}
</div>
<br/>
@ -158,7 +185,7 @@ templ TrackEmbed(prefs cfg.Preferences, t sc.Track, stream string, displayErr st
<img src={ t.Artwork } width="300px"/>
}
<h1>{ t.Title }</h1>
@TrackPlayer(prefs, t, stream, displayErr, false, nil, nil, "", "")
@TrackPlayer(prefs, t, stream, displayErr, false, nil, nil, "", "", "")
@UserItem(&t.Author)
</body>
</html>

View file

@ -214,7 +214,7 @@ templ UserRelated(prefs cfg.Preferences, u sc.User, r []*sc.User) {
templ UserTopTracks(prefs cfg.Preferences, u sc.User, t []*sc.Track) {
@UserBase(prefs, u)
@UserButtons("popular-tracks", u.Permalink)
@UserButtons("popular tracks", u.Permalink)
<br/>
if len(t) != 0 {
<div>