mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2025-12-10 13:49:39 +05:00
Compare commits
33 Commits
revampire
...
0cf9f0d53f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf9f0d53f | ||
|
|
fd571bc23c | ||
|
|
0474290010 | ||
|
|
e0bfcaadba | ||
|
|
39787b6cc9 | ||
|
|
92500f4a41 | ||
|
|
a28968af97 | ||
|
|
2ab73d517d | ||
|
|
cf369e3f28 | ||
|
|
5e96cee22e | ||
|
|
a0551d742b | ||
|
|
3d96470a05 | ||
|
|
d2117e5182 | ||
|
|
0ca7a3986a | ||
|
|
a2d6d501b3 | ||
|
|
4f6f6585cb | ||
|
|
14b501d162 | ||
|
|
a61c15bb7d | ||
|
|
4df1bc4f17 | ||
|
|
8c292de7c7 | ||
|
|
5d1168f7d7 | ||
|
|
3b63615014 | ||
|
|
8fd150ff96 | ||
|
|
280e551230 | ||
|
|
2b84ba8748 | ||
|
|
7a044a2dec | ||
|
|
3718ef7e66 | ||
|
|
6daeac4638 | ||
|
|
810661742b | ||
|
|
679731a2cb | ||
|
|
e2b7e9aad6 | ||
|
|
12a3d850a5 | ||
|
|
0ac5523d22 |
44
Dockerfile
44
Dockerfile
@@ -1,4 +1,4 @@
|
||||
ARG GO_VERSION=1.24.0
|
||||
ARG GO_VERSION=1.25.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||
ARG TARGETOS
|
||||
@@ -6,35 +6,33 @@ ARG TARGETARCH
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN go env -w GOPROXY=direct
|
||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||
RUN go install github.com/dlclark/regexp2cg@main
|
||||
RUN go env -w GOPROXY=direct && \
|
||||
mkdir /etc2 && \
|
||||
mkdir /etc2/ssl && mkdir /etc2/ssl/certs && \
|
||||
cp /etc/ssl/certs/ca-certificates.crt /etc2/ssl/certs/ca-certificates.crt && \
|
||||
echo "soundcloak:x:5000:5000:Soundcloak user:/:/sbin/nologin" > /etc2/passwd && \
|
||||
echo "soundcloak:x:5000:" > /etc2/group
|
||||
|
||||
COPY go.* .
|
||||
RUN go mod download -x && \
|
||||
go install -v github.com/a-h/templ/cmd/templ@latest && \
|
||||
go install -v github.com/dlclark/regexp2cg@main
|
||||
COPY . .
|
||||
# usually soundcloakctl updates together with soundcloak, so we should redownload it
|
||||
RUN go install git.maid.zone/stuff/soundcloakctl@master
|
||||
RUN soundcloakctl js download
|
||||
|
||||
RUN templ generate
|
||||
RUN go generate ./lib/*
|
||||
RUN soundcloakctl config codegen
|
||||
RUN soundcloakctl -nozstd -notable precompress
|
||||
|
||||
RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} GOOS=${TARGETOS} go build -ldflags "-s -w -extldflags '-static'" -o ./app
|
||||
RUN echo "soundcloak:x:5000:5000:Soundcloak user:/:/sbin/nologin" > /etc/minimal-passwd && \
|
||||
echo "soundcloak:x:5000:" > /etc/minimal-group
|
||||
|
||||
RUN soundcloakctl postbuild
|
||||
RUN go install -v git.maid.zone/stuff/soundcloakctl@master && \
|
||||
soundcloakctl js download && \
|
||||
templ generate && \
|
||||
go generate ./lib/* && \
|
||||
soundcloakctl config codegen && \
|
||||
soundcloakctl -nozstd precompress && \
|
||||
CGO_ENABLED=0 GOARCH=${TARGETARCH} GOOS=${TARGETOS} go build -v -ldflags "-s -w -extldflags '-static' -X git.maid.zone/stuff/soundcloak/lib/cfg.Commit=`git rev-parse HEAD | head -c 7` -X git.maid.zone/stuff/soundcloak/lib/cfg.Repo=`git remote get-url origin`" -o ./app && \
|
||||
soundcloakctl postbuild
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /build/static/assets /static/assets
|
||||
COPY --from=build /build/static/instance /static/instance
|
||||
COPY --from=build /build/static/external /static/external
|
||||
COPY --from=build /build/static/ /static/
|
||||
COPY --from=build /build/app /app
|
||||
COPY --from=build /etc/minimal-passwd /etc/passwd
|
||||
COPY --from=build /etc/minimal-group /etc/group
|
||||
COPY --from=build /etc2/ /etc/
|
||||
|
||||
EXPOSE 4664
|
||||
|
||||
|
||||
@@ -23,5 +23,7 @@ Frontend for SoundCloud
|
||||
## [Instance Maintainer Guide](docs/INSTANCE_GUIDE.md)
|
||||
## [Development Guide](docs/DEV_GUIDE.md)
|
||||
|
||||
If you have any questions, or just wanna talk about soundcloak, you can join the maid.zone XMPP/Matrix chat: [public@muc.maid.zone](xmpp:public@muc.maid.zone?join) / [#public:maid.zone](https://matrix.to/#/#public:maid.zone)
|
||||
|
||||
# Notice
|
||||
soundcloak is not affiliated with SoundCloud.
|
||||
2
build
2
build
@@ -1,3 +1,3 @@
|
||||
templ generate
|
||||
go generate ./lib/*
|
||||
go build main.go
|
||||
go build -ldflags "-X git.maid.zone/stuff/soundcloak/lib/cfg.Commit=`git rev-parse HEAD | head -c 7` -X git.maid.zone/stuff/soundcloak/lib/cfg.Repo=`git remote get-url origin`" main.go
|
||||
82
docs/API.md
Normal file
82
docs/API.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Check enabled features
|
||||
|
||||
Just go to `/_/info` endpoint
|
||||
|
||||
# API
|
||||
|
||||
To make use of it, instance must have API enabled of course. All responses are in JSON format if the request is successful. Errors are just returned as plaintext. Currently, there is few functionality present. If you are working on some cool project and wanna have more functionality here, let me know
|
||||
|
||||
## Paginated endpoints
|
||||
|
||||
Those endpoints return an object with properties:
|
||||
- `collection`: List of the results on this page
|
||||
- `total_results`: total results duh, maybe 0 on some endpoints
|
||||
- `next_href`: Link to next page
|
||||
|
||||
To go to next page, take the `next_href`, strip away everything until `?`, and pass that as `pagination` query parameter. Note that not all endpoints may support going to next page.
|
||||
|
||||
You can also use `pagination` parameter to pass raw arguments into soundcloud api (for example, more advanced search filters, different initial result limit, etc)
|
||||
|
||||
## GET `/_/api/search`
|
||||
|
||||
Search for users, tracks or playlists. Query parameters are:
|
||||
- `q`: the query
|
||||
- `type`: `users`, `tracks`, or `playlists`. Required
|
||||
- `pagination`: [Read above](#paginated-endpoints)
|
||||
|
||||
For example: `/_/api/search?q=test&type=tracks` to search for `tracks` named `test`
|
||||
|
||||
## GET `/_/api/track/:id`
|
||||
|
||||
Get track by ID.
|
||||
|
||||
For example: `/_/api/track/2014143543` to get track with ID `2014143543`
|
||||
|
||||
## GET `/_/api/track/:id/related`
|
||||
|
||||
Get related tracks by ID. Pagination is supported here. Initial request returns upto 20 tracks
|
||||
|
||||
For example: `/_/api/track/2014143543/related` to get tracks related to track with ID `2014143543`
|
||||
|
||||
## GET `/_/api/tracks`
|
||||
|
||||
Get tracks by ID in bulk. Pass the IDs comma-separated as `ids` query parameter. You can't request more than 50 tracks at once. The result is a list, which only contains the tracks which were successfully resolved
|
||||
|
||||
For example: `/_/api/tracks?ids=2014143543,476907846`. This will only return one track, since 2nd ID is not a track ID
|
||||
|
||||
## GET `/_/api/playlistByPermalink/:author/sets/:playlist`
|
||||
|
||||
Get playlist by permalinks.
|
||||
|
||||
For example: `/_/api/playlistByPermalink/lucybedroque/sets/unmusique` to get `unmusique` playlist from `lucybedroque`
|
||||
|
||||
## GET `/_/api/playlistByPermalink/:author/sets/:playlist/tracks`
|
||||
|
||||
Get list of track IDs in playlist.
|
||||
|
||||
For example: `/_/api/playlistByPermalink/lucybedroque/sets/unmusique/tracks` to get all IDs of the tracks in playlist `unmusique` from `lucybedroque`
|
||||
|
||||
# Other automation
|
||||
|
||||
Doesn't require API to be enabled
|
||||
|
||||
## GET `/_/restream/:author/:track`
|
||||
|
||||
Restream must be enabled in the instance. This endpoint can be used to download or stream tracks. Query parameters are:
|
||||
- `metadata`: `true` or `false`. If `true`, soundcloak will inject metadata (author, track cover, track title, etc) into the audio file, but this may take a little bit more time
|
||||
- `audio`: `best`, `aac`, `opus`, or `mpeg`. [Read more here](AUDIO_PRESETS.md)
|
||||
|
||||
Restream converts the HLS playlist to an audio file on the fly serverside, optionally adding metadata. Please note that when `audio` is `opus` and `metadata` is `true`, it's not done on the fly, as metadata injection is a bit tricky there.
|
||||
|
||||
For example: `/_/restream/lucybedroque/speakers?metadata=true&audio=opus` to get the `opus` audio with `metadata` for song `speakers` by author `lucybedroque`
|
||||
|
||||
## GET `/_/searchSuggestions`
|
||||
|
||||
Pass your query as `q` query parameter
|
||||
|
||||
For example: `/_/searchSuggestions?q=hi` to get search suggestions for `hi`
|
||||
|
||||
## GET `/_/proxy/images`
|
||||
|
||||
ProxyImages must be enabled in the instance. Put image url into `url` query parameter. Of course, this only proxies images from soundcloud cdn
|
||||
|
||||
@@ -105,4 +105,6 @@ If you want to add a new feature that's not in [the todo list](https://git.maid.
|
||||
|
||||
If you have updated go dependencies or added new ones, please run `go mod tidy` before commiting.
|
||||
|
||||
If you update structs, please run [betteralign](https://github.com/dkorunic/betteralign) to make sure memory layout is optimized.
|
||||
|
||||
Any security vulnerabilities should first be disclosed privately to the maintainer ([different ways to contact me are listed here](https://laptopc.at))
|
||||
@@ -60,7 +60,7 @@ git pull
|
||||
2. Stop the container:
|
||||
|
||||
```sh
|
||||
docker container stop soundcloak
|
||||
docker compose down
|
||||
```
|
||||
|
||||
3. Build the container with updated source code:
|
||||
@@ -115,18 +115,35 @@ Some notes:
|
||||
| TrackCacheCleanDelay | TRACK_CACHE_CLEAN_DELAY | 5 minutes | Time between each cleanup of the cache (to remove expired tracks) |
|
||||
| PlaylistTTL | PLAYLIST_TTL | 20 minutes | Time until Playlist data cache expires |
|
||||
| PlaylistCacheCleanDelay | PLAYLIST_CACHE_CLEAN_DELAY | 5 minutes | Time between each cleanup of the cache (to remove expired playlists) |
|
||||
| UserAgent | USER_AGENT | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3 | User-Agent header used for requests to SoundCloud |
|
||||
| UserAgent | USER_AGENT | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 | User-Agent header used for requests to SoundCloud |
|
||||
| ClientID | CLIENT_ID | (empty) | Authorization token for requests to SoundCloud. It's automatically extracted from the current version of the website, but you can override it if there are issues |
|
||||
| DNSCacheTTL | DNS_CACHE_TTL | 60 minutes | Time until DNS cache expires |
|
||||
| EnableAPI | ENABLE_API | false | Should [API](API.md) be enabled? |
|
||||
| Network | NETWORK | tcp4 | Network to listen on. Can be tcp4, tcp6 or unix |
|
||||
| Addr | ADDR | :4664 | Address and port for soundcloak to listen on |
|
||||
| Addr | ADDR | :4664 | Address and port (or socket path) for soundcloak to listen on |
|
||||
| UnixSocketPerms | UNIX_SOCKET_PERMS | 0775 | Permissions for unix socket (Network must be set to unix) |
|
||||
| Prefork | PREFORK | false | Run multiple instances of soundcloak locally to be able to handle more requests. Each one will be a separate process, so they will have separate cache. |
|
||||
| TrustedProxyCheck | TRUSTED_PROXY_CHECK | true | Use X-Forwarded-* headers if IP is in TrustedProxies list. When disabled, those headers will blindly be used. |
|
||||
| TrustedProxies | TRUSTED_PROXIES | [] | List of IPs or IP ranges of trusted proxies |
|
||||
| CodegenConfig | CODEGEN_CONFIG | false | Highly recommended to enable. Embeds the config into the binary, which helps reduce size if you aren't using certain features and generally optimize the binary better. Keep in mind that you will have to rebuild the image/binary each time you want to change config. (Note: you need to run `soundcloakctl config codegen` or use docker, as it runs it for you) |
|
||||
| EmbedFiles | EMBED_FILES | false | Embed files into the binary. Keep in mind that you will have to rebuild the image/binary each time static files are changed (e.g. custom instance files) |
|
||||
| CodegenConfig | CODEGEN_CONFIG | false | Embeds the config into the binary, which helps reduce size if you aren't using certain features and generally optimize the binary better. Keep in mind that you will have to rebuild the image/binary each time you want to change config. (Note: you need to run `soundcloakctl config codegen` or use docker, as it runs it for you) |
|
||||
| EmbedFiles | EMBED_FILES | true | Embed files into the binary. Keep in mind that you will have to rebuild the image/binary each time static files are changed (e.g. custom instance files) |
|
||||
|
||||
</details>
|
||||
|
||||
## Potential issues
|
||||
|
||||
### Status code 403/429
|
||||
Your IP address did too many requests to SoundCloud and got blocked for a bit of time.
|
||||
|
||||
### script/version/clientid not found
|
||||
soundcloak failed to extract the ClientID token from SoundCloud.
|
||||
This may happen due to your due to changes made by SoundCloud, or IP getting blocked.
|
||||
|
||||
In case your IP is not blocked, please report this issue to me on [Codeberg](https://codeberg.org/maid-zone/soundcloak/issues/new) or [GitHub](https://github.com/maid-zone/soundcloak/issues/new) or elsewhere
|
||||
|
||||
As a temporary fix, you can go to SoundCloud website in your browser, open the Developer Tools, perform some actions (like loading a profile or searching) and check the Network tab.
|
||||
There you will see requests to `api-v2.soundcloud.com` with `client_id` parameter. You can use this token with the `ClientID` setting in soundcloak
|
||||
|
||||
## Tinkering with the frontend
|
||||
|
||||
<details>
|
||||
@@ -134,6 +151,9 @@ Some notes:
|
||||
|
||||
I will mainly talk about the static files here. Maybe about the templates too in the future
|
||||
|
||||
Keep in mind that by default soundcloak will embed all static files into the built binary. You will need to rebuild each time you want to change something.
|
||||
Or you can also set `EmbedFiles` to `false`
|
||||
|
||||
The static files are stored in `static/assets` folder
|
||||
|
||||
### Overriding files
|
||||
@@ -169,7 +189,7 @@ To get listed on [the instance list](https://maid.zone/soundcloak/instances.html
|
||||
Basic rules:
|
||||
|
||||
1. Do not collect user information (either yourself, or by including 3rd party tooling which does that)
|
||||
2. If you are modifying the source code, publish those changes somewhere. Even if it's just static files, it would be best to publish those changes somewhere.
|
||||
2. If you are modifying the source code, publish those changes somewhere.
|
||||
|
||||
Also, keep in mind that the instance list will periodically hit the `/_/info` endpoint on your instance (usually each 10 minutes) in order to display the instance settings. If you do not want this to happen, state it in your discussion/message, and I will exclude your instance from this checking.
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
# Preferences
|
||||
|
||||
|
||||
| Name | Key | Default | Possible values | Description |
|
||||
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Parse descriptions | ParseDescriptions | true | true, false | Turn @mentions, external links (https://example.org) and emails (hello@example.org) inside descriptions into clickable links |
|
||||
| Show current audio | ShowAudio | false | true, false | Show what [audio preset](AUDIO_PRESETS.md) is being streamed below the audio player |
|
||||
| Proxy images | ProxyImages | same as ProxyImages in backend config | true, false | Proxy images through the backend. ProxyImages must be enabled on the backend |
|
||||
| Download audio | DownloadAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when downloading audio with metadata. Restream must be enabled on the backend |
|
||||
| Autoplay next track in playlists | AutoplayNextTrack | false | true, false | Automatically start playlist playback when you open a track from the playlist. Requires JS |
|
||||
| Default autoplay mode | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for autoplay. Normal - play songs in order. Random - play random song next |
|
||||
| Fetch search suggestions | SearchSuggestions | false | true, false | Load search suggestions on main page when you type. Requires JS |
|
||||
| Dynamically load comments | DynamicLoadComments | false | true, false | Dynamically load track comments, without leaving the page. Requires JS |
|
||||
| Player | Player | "restream" if Restream is enabled in backend config, otherwise - "hls" | "restream", "hls", "none" | Method used to play the track in the frontend. HLS - requires JavaScript, loads the track in pieces. Restream - works without JavaScript, loads entire track through the backend right away. None - don't play the track |
|
||||
| Download audio | DownloadAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when downloading audio with metadata. Restream must be enabled on the backend |
|
||||
|
||||
## Player-specific preferences
|
||||
# Player preferences
|
||||
|
||||
### HLS Player
|
||||
| Name | Key | Default | Possible values | Description |
|
||||
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Player | Player | "restream" if Restream is enabled in backend config, otherwise - "hls" | "restream", "hls", "none" | Method used to play the track in the frontend. HLS - requires JavaScript, loads the track in pieces. Restream - works without JavaScript, loads entire track through the backend right away. None - don't play the track |
|
||||
|
||||
## HLS Player
|
||||
|
||||
| Name | Key | Default | Possible values | Description |
|
||||
| :-------------------- | ------------------- | ---------------------------------------- | ----------------- | :------------------------------------------------------------------------------------ |
|
||||
@@ -23,9 +18,30 @@
|
||||
| Fully preload track | FullyPreloadTrack | false | true, false | Fully load track when the page is loaded (track stream expires in ~5 minutes) |
|
||||
| Streaming audio | HLSAudio | "mpeg" | "mpeg", "aac" | What [audio preset](AUDIO_PRESETS.md) should be loaded when streaming audio |
|
||||
|
||||
### Restream Player
|
||||
## Restream Player
|
||||
|
||||
|
||||
| Name | Key | Default | Possible values | Description |
|
||||
| :---------------- | --------------- | --------- | ------------------------------- | :--------------------------------------------------------------------------- |
|
||||
| Streaming audio | RestreamAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when streaming audio |
|
||||
|
||||
|
||||
# Frontend enhancements
|
||||
|
||||
| Name | Key | Default | Possible values | Description |
|
||||
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Proxy images | ProxyImages | same as ProxyImages in backend config | true, false | Proxy images through the backend. ProxyImages must be enabled on the backend |
|
||||
| Parse descriptions | ParseDescriptions | true | true, false | Turn @mentions, external links (https://example.org) and emails (hello@example.org) inside descriptions into clickable links |
|
||||
| Show current audio | ShowAudio | false | true, false | Show what [audio preset](AUDIO_PRESETS.md) is being streamed below the audio player |
|
||||
| Fetch search suggestions | SearchSuggestions | false | true, false | Load search suggestions on main page when you type. Requires JS |
|
||||
| Dynamically load comments | DynamicLoadComments | false | true, false | Dynamically load track comments, without leaving the page. Requires JS
|
||||
| Keep player focus | KeepPlayerFocus | false | true, false | Always keep track element in focus, so you can control it with keyboard. Requires JS |
|
||||
|
||||
## Autoplay
|
||||
*Requires JS. You also need to allow autoplay from this domain*
|
||||
|
||||
| Name | Key | Default | Possible values | Description |
|
||||
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Autoplay next track in playlists | AutoplayNextTrack | false | true, false | Automatically start playlist playback when you open a track from the playlist. |
|
||||
| Default autoplay mode (in playlists) | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for playlist autoplay. Normal - play songs in order. Random - play random song next |
|
||||
| Autoplay next related track | AutoplayNextRelatedTrack | false | true, false | Automatically play a related track next.
|
||||
@@ -23,3 +23,9 @@ Scroll down to the end of the preferences page. There you can see a management t
|
||||
# Redirecting from SoundCloud to soundcloak
|
||||
|
||||
soundcloak tries to keep the URL schemes same to SoundCloud's, so you can just replace `soundcloud.com` with your instance URL. For short links: `https://on.soundcloud.com/boiKDP46fayYDoVK9` -> `<instance>/on/boiKDP46fayYDoVK9`
|
||||
|
||||
To automatically redirect, you can use [LibRedirect](https://libredirect.github.io/) extension. Soundcloak is supported
|
||||
|
||||
# Extra notes
|
||||
|
||||
If you find music that you like, make sure to download it! Stuff that's on there may be deleted or changed at any moment, without any warning or ability to experience it again, unless you download it for yourself. Download button is available if `Restream` is enabled in backend config. You can configure audio preset for downloading in preferences page. For easily and quickly downloading entire users or playlists, you can use my tool [scrip](https://git.maid.zone/laptop/scrip)
|
||||
31
go.mod
31
go.mod
@@ -1,37 +1,34 @@
|
||||
module git.maid.zone/stuff/soundcloak
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.833
|
||||
github.com/a-h/templ v0.3.943
|
||||
github.com/bogem/id3v2/v2 v2.1.4
|
||||
github.com/dlclark/regexp2 v1.11.5-0.20240806004527-5bbbed8ea10b
|
||||
github.com/gcottom/mp4meta v0.0.5
|
||||
github.com/gcottom/oggmeta v0.0.8
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4
|
||||
github.com/valyala/fasthttp v1.58.0
|
||||
github.com/gofiber/fiber/v3 v3.0.0-rc.1
|
||||
github.com/valyala/fasthttp v1.65.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/abema/go-mp4 v1.4.1 // indirect
|
||||
github.com/aler9/writerseeker v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gofiber/schema v1.3.0 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.2.5 // indirect
|
||||
github.com/tinylib/msgp v1.4.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
64
go.sum
64
go.sum
@@ -1,11 +1,11 @@
|
||||
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
||||
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
|
||||
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/abema/go-mp4 v1.4.1 h1:YoS4VRqd+pAmddRPLFf8vMk74kuGl6ULSjzhsIqwr6M=
|
||||
github.com/abema/go-mp4 v1.4.1/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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
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=
|
||||
@@ -14,27 +14,27 @@ 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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gcottom/mp4meta v0.0.5 h1:pZZAMwRMisx7RaewO7MvjuD3t+tHCYRujpJkQI2yVHU=
|
||||
github.com/gcottom/mp4meta v0.0.5/go.mod h1:Dxt8rM1fJDl9sOJCfsnQuprE3gWtmE/oXTJHA/g5WHY=
|
||||
github.com/gcottom/oggmeta v0.0.8 h1:cai8PX7k4/6coKaYCeBZI5GD2f+bgzrCHbcFM5l5Vms=
|
||||
github.com/gcottom/oggmeta v0.0.8/go.mod h1:as5q4K3n3GHJIuWKoJPjiroxPEtqDCNB52+sYzBSaGQ=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk=
|
||||
github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q=
|
||||
github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-rc.1 h1:034MxesK6bqGkidP+QR+Ysc1ukOacBWOHCarCKC1xfg=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-rc.1/go.mod h1:hFdT00oT0XVuQH1/z2i5n1pl/msExHDUie1SsLOkCuM=
|
||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.1 h1:b77K5Rk9+Pjdxz4HlwEBnS7u5nikhx7armQB8xPds4s=
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg93912R/tbKWhn6V3E=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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=
|
||||
@@ -45,27 +45,27 @@ 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/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/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
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/shamaton/msgpack/v2 v2.3.0 h1:eawIa7lQmwRv0V6rdmL/5Ev9KdJHk07eQH3ceJi3BUw=
|
||||
github.com/shamaton/msgpack/v2 v2.3.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
||||
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
||||
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=
|
||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -73,14 +73,14 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
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=
|
||||
@@ -90,16 +90,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
|
||||
117
lib/api/init.go
Normal file
117
lib/api/init.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func Load(a *fiber.App) {
|
||||
r := a.Group("/_/api")
|
||||
|
||||
prefs := cfg.Preferences{ProxyImages: &cfg.False}
|
||||
r.Get("/search", func(c fiber.Ctx) error {
|
||||
q := cfg.B2s(c.RequestCtx().QueryArgs().Peek("q"))
|
||||
t := cfg.B2s(c.RequestCtx().QueryArgs().Peek("type"))
|
||||
args := cfg.B2s(c.RequestCtx().QueryArgs().Peek("pagination"))
|
||||
if args == "" {
|
||||
args = "?q=" + url.QueryEscape(q)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "tracks":
|
||||
p, err := sc.SearchTracks("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting tracks for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
|
||||
case "users":
|
||||
p, err := sc.SearchUsers("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting users for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
|
||||
case "playlists":
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting playlists for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
}
|
||||
|
||||
return c.SendStatus(404)
|
||||
})
|
||||
|
||||
r.Get("/track/:id/related", func(c fiber.Ctx) error {
|
||||
args := cfg.B2s(c.RequestCtx().QueryArgs().Peek("pagination"))
|
||||
if args == "" {
|
||||
args = "?limit=20"
|
||||
}
|
||||
|
||||
p, err := (sc.Track{ID: json.Number(c.Params("id"))}).GetRelated("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting related tracks for %s: %s\n", c.Params("id"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
})
|
||||
|
||||
r.Get("/playlistByPermalink/:author/sets/:playlist", func(c fiber.Ctx) error {
|
||||
p, err := sc.GetPlaylist("", c.Params("author")+"/sets/"+c.Params("playlist"))
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting %s playlist from %s: %s\n", c.Params("playlist"), c.Params("author"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
})
|
||||
|
||||
r.Get("/playlistByPermalink/:author/sets/:playlist/tracks", func(c fiber.Ctx) error {
|
||||
p, err := sc.GetPlaylist("", c.Params("author")+"/sets/"+c.Params("playlist"))
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting %s playlist tracks from %s: %s\n", c.Params("playlist"), c.Params("author"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
tracks := make([]json.Number, len(p.Tracks))
|
||||
for i, t := range p.Tracks {
|
||||
tracks[i] = t.ID
|
||||
}
|
||||
|
||||
return c.JSON(tracks)
|
||||
})
|
||||
|
||||
r.Get("/track/:id", func(c fiber.Ctx) error {
|
||||
t, err := sc.GetTrackByID("", c.Params("id"))
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting track %s: %s\n", c.Params("id"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(t)
|
||||
})
|
||||
|
||||
r.Get("/tracks", func(c fiber.Ctx) error {
|
||||
ids := cfg.B2s(c.RequestCtx().QueryArgs().Peek("ids"))
|
||||
t, err := sc.GetTracks("", ids)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting %s tracks: %s\n", ids, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(t)
|
||||
})
|
||||
}
|
||||
103
lib/cfg/init.go
103
lib/cfg/init.go
@@ -1,7 +1,6 @@
|
||||
package cfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -65,11 +64,17 @@ var PlaylistTTL = 20 * time.Minute
|
||||
var PlaylistCacheCleanDelay = PlaylistTTL / 4
|
||||
|
||||
// default fasthttp one was causing connections to be stuck? todo make it cycle browser useragents or just choose random at startup
|
||||
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3"
|
||||
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
|
||||
|
||||
// override the extractor
|
||||
var ClientID = ""
|
||||
|
||||
// time-to-live for dns cache
|
||||
var DNSCacheTTL = 60 * time.Minute
|
||||
|
||||
// enab;e api
|
||||
var EnableAPI = false
|
||||
|
||||
// // // some webserver configuration, put here to make it easier to configure what you need // // //
|
||||
// more info can be found here: https://docs.gofiber.io/api/fiber#config
|
||||
|
||||
@@ -80,6 +85,9 @@ var Network = "tcp4"
|
||||
// run soundcloak on this address (localhost:4664 by default)
|
||||
var Addr = ":4664"
|
||||
|
||||
// unix socket perms wow
|
||||
var UnixSocketPerms os.FileMode = 0775
|
||||
|
||||
// run multiple instances of soundcloak locally to be able to handle more requests
|
||||
// each one will be a separate process, so they will have separate cache
|
||||
var Prefork = false
|
||||
@@ -95,7 +103,7 @@ var TrustedProxies = []string{}
|
||||
var CodegenConfig = false
|
||||
|
||||
// use static files embedded in binary
|
||||
var EmbedFiles = false
|
||||
var EmbedFiles = true
|
||||
|
||||
// // end of config // //
|
||||
|
||||
@@ -130,6 +138,7 @@ func defaultPreferences() {
|
||||
|
||||
DefaultPreferences.ParseDescriptions = &True
|
||||
DefaultPreferences.AutoplayNextTrack = &False
|
||||
DefaultPreferences.AutoplayNextRelatedTrack = &False
|
||||
|
||||
p2 := AutoplayNormal
|
||||
DefaultPreferences.DefaultAutoplayMode = &p2
|
||||
@@ -143,6 +152,7 @@ func defaultPreferences() {
|
||||
|
||||
DefaultPreferences.SearchSuggestions = &False
|
||||
DefaultPreferences.DynamicLoadComments = &False
|
||||
DefaultPreferences.KeepPlayerFocus = &False
|
||||
}
|
||||
|
||||
func loadDefaultPreferences(loaded Preferences) {
|
||||
@@ -188,6 +198,12 @@ func loadDefaultPreferences(loaded Preferences) {
|
||||
DefaultPreferences.AutoplayNextTrack = &False
|
||||
}
|
||||
|
||||
if loaded.AutoplayNextRelatedTrack != nil {
|
||||
DefaultPreferences.AutoplayNextRelatedTrack = loaded.AutoplayNextRelatedTrack
|
||||
} else {
|
||||
DefaultPreferences.AutoplayNextRelatedTrack = &False
|
||||
}
|
||||
|
||||
if loaded.DefaultAutoplayMode != nil {
|
||||
DefaultPreferences.DefaultAutoplayMode = loaded.DefaultAutoplayMode
|
||||
} else {
|
||||
@@ -231,21 +247,18 @@ func loadDefaultPreferences(loaded Preferences) {
|
||||
} else {
|
||||
DefaultPreferences.DynamicLoadComments = &False
|
||||
}
|
||||
|
||||
if loaded.KeepPlayerFocus != nil {
|
||||
DefaultPreferences.KeepPlayerFocus = loaded.KeepPlayerFocus
|
||||
} else {
|
||||
DefaultPreferences.KeepPlayerFocus = &False
|
||||
}
|
||||
}
|
||||
|
||||
func boolean(in string) bool {
|
||||
return strings.Trim(strings.ToLower(in), " ") == "true"
|
||||
}
|
||||
|
||||
type wrappedError struct {
|
||||
err error
|
||||
fault string
|
||||
}
|
||||
|
||||
func (w wrappedError) Error() string {
|
||||
return fmt.Sprintf("error loading %s: %s", w.fault, w.err)
|
||||
}
|
||||
|
||||
func fromEnv() error {
|
||||
env := os.Getenv("GET_WEB_PROFILES")
|
||||
if env != "" {
|
||||
@@ -257,7 +270,7 @@ func fromEnv() error {
|
||||
var p Preferences
|
||||
err := json.Unmarshal(S2b(env), &p)
|
||||
if err != nil {
|
||||
return wrappedError{err, "DEFAULT_PREFERENCES"}
|
||||
return err
|
||||
}
|
||||
|
||||
loadDefaultPreferences(p)
|
||||
@@ -299,7 +312,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "CLIENT_ID_TTL"}
|
||||
return err
|
||||
}
|
||||
|
||||
ClientIDTTL = time.Duration(num) * time.Second
|
||||
@@ -309,7 +322,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "USER_TTL"}
|
||||
return err
|
||||
}
|
||||
|
||||
UserTTL = time.Duration(num) * time.Second
|
||||
@@ -319,7 +332,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "USER_CACHE_CLEAN_DELAY"}
|
||||
return err
|
||||
}
|
||||
|
||||
UserCacheCleanDelay = time.Duration(num) * time.Second
|
||||
@@ -329,7 +342,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "TRACK_TTL"}
|
||||
return err
|
||||
}
|
||||
|
||||
TrackTTL = time.Duration(num) * time.Second
|
||||
@@ -339,7 +352,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "TRACK_CACHE_CLEAN_DELAY"}
|
||||
return err
|
||||
}
|
||||
|
||||
TrackCacheCleanDelay = time.Duration(num) * time.Second
|
||||
@@ -349,7 +362,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "PLAYLIST_TTL"}
|
||||
return err
|
||||
}
|
||||
|
||||
PlaylistTTL = time.Duration(num) * time.Second
|
||||
@@ -359,7 +372,7 @@ func fromEnv() error {
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "PLAYLIST_CACHE_CLEAN_DELAY"}
|
||||
return err
|
||||
}
|
||||
|
||||
PlaylistCacheCleanDelay = time.Duration(num) * time.Second
|
||||
@@ -370,16 +383,26 @@ func fromEnv() error {
|
||||
UserAgent = env
|
||||
}
|
||||
|
||||
env = os.Getenv("CLIENT_ID")
|
||||
if env != "" {
|
||||
ClientID = env
|
||||
}
|
||||
|
||||
env = os.Getenv("DNS_CACHE_TTL")
|
||||
if env != "" {
|
||||
num, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return wrappedError{err, "DNS_CACHE_TTL"}
|
||||
return err
|
||||
}
|
||||
|
||||
DNSCacheTTL = time.Duration(num) * time.Second
|
||||
}
|
||||
|
||||
env = os.Getenv("ENABLE_API")
|
||||
if env != "" {
|
||||
EnableAPI = boolean(env)
|
||||
}
|
||||
|
||||
env = os.Getenv("NETWORK")
|
||||
if env != "" {
|
||||
Network = env
|
||||
@@ -390,6 +413,16 @@ func fromEnv() error {
|
||||
Addr = env
|
||||
}
|
||||
|
||||
env = os.Getenv("UNIX_SOCKET_PERMS")
|
||||
if env != "" {
|
||||
p, err := strconv.ParseUint(env, 0, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
UnixSocketPerms = os.FileMode(p)
|
||||
}
|
||||
|
||||
env = os.Getenv("PREFORK")
|
||||
if env != "" {
|
||||
Prefork = boolean(env)
|
||||
@@ -405,7 +438,7 @@ func fromEnv() error {
|
||||
var p []string
|
||||
err := json.Unmarshal(S2b(env), &p)
|
||||
if err != nil {
|
||||
return wrappedError{err, "TRUSTED_PROXIES"}
|
||||
return err
|
||||
}
|
||||
|
||||
TrustedProxies = p
|
||||
@@ -429,12 +462,8 @@ func init() {
|
||||
if env := os.Getenv("SOUNDCLOAK_CONFIG"); env == "FROM_ENV" {
|
||||
err := fromEnv()
|
||||
if err != nil {
|
||||
// So we only set default preferences if it fails to load that in
|
||||
if err.(wrappedError).fault == "DEFAULT_PREFERENCES" {
|
||||
defaultPreferences()
|
||||
}
|
||||
|
||||
log.Println("failed to load config from environment:", err)
|
||||
log.Println("Warning: failed to load config from environment:", err)
|
||||
defaultPreferences()
|
||||
}
|
||||
|
||||
return
|
||||
@@ -444,7 +473,7 @@ func init() {
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("failed to load config from %s: %s\n", filename, err)
|
||||
log.Printf("Warning: failed to load config from %s: %s\n", filename, err)
|
||||
defaultPreferences()
|
||||
return
|
||||
}
|
||||
@@ -467,8 +496,10 @@ func init() {
|
||||
PlaylistCacheCleanDelay *time.Duration
|
||||
UserAgent *string
|
||||
DNSCacheTTL *time.Duration
|
||||
EnableAPI *bool
|
||||
Network *string
|
||||
Addr *string
|
||||
UnixSocketPerms *string
|
||||
Prefork *bool
|
||||
TrustedProxyCheck *bool
|
||||
TrustedProxies *[]string
|
||||
@@ -533,12 +564,23 @@ func init() {
|
||||
if config.DNSCacheTTL != nil {
|
||||
DNSCacheTTL = *config.DNSCacheTTL * time.Second
|
||||
}
|
||||
if config.EnableAPI != nil {
|
||||
EnableAPI = *config.EnableAPI
|
||||
}
|
||||
if config.Network != nil {
|
||||
Network = *config.Network
|
||||
}
|
||||
if config.Addr != nil {
|
||||
Addr = *config.Addr
|
||||
}
|
||||
if config.UnixSocketPerms != nil {
|
||||
p, err := strconv.ParseUint(*config.UnixSocketPerms, 0, 32)
|
||||
if err != nil {
|
||||
log.Println("failed to parse UnixSocketPerms:", err)
|
||||
} else {
|
||||
UnixSocketPerms = os.FileMode(p)
|
||||
}
|
||||
}
|
||||
if config.Prefork != nil {
|
||||
Prefork = *config.Prefork
|
||||
}
|
||||
@@ -563,6 +605,3 @@ func init() {
|
||||
}
|
||||
|
||||
const Debug = false
|
||||
const Commit = "unknown"
|
||||
const Repo = "unknown"
|
||||
const CommitURL = "unknown"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cfg
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -17,6 +19,13 @@ const MaxIdleConnDuration = 4 * time.Hour
|
||||
var True = true
|
||||
var False = false
|
||||
|
||||
// embedded at buildtime
|
||||
var Commit = "unknown"
|
||||
var Repo = "unknown"
|
||||
|
||||
// generated at runtime
|
||||
var CommitURL = "unknown"
|
||||
|
||||
const (
|
||||
// Downloads the HLS stream on the backend, and restreams it to frontend as a file. Requires no JS, but less stable client-side
|
||||
RestreamPlayer string = "restream"
|
||||
@@ -64,6 +73,9 @@ type Preferences struct {
|
||||
// Automatically play next track in playlists
|
||||
AutoplayNextTrack *bool
|
||||
|
||||
// Automatically play next related track
|
||||
AutoplayNextRelatedTrack *bool
|
||||
|
||||
DefaultAutoplayMode *string // "normal" or "random"
|
||||
|
||||
// Check above for more info
|
||||
@@ -77,6 +89,8 @@ type Preferences struct {
|
||||
SearchSuggestions *bool // load search suggestions on main page
|
||||
|
||||
DynamicLoadComments *bool // dynamic comments loader without leaving track page
|
||||
|
||||
KeepPlayerFocus *bool // keep player element in focus
|
||||
}
|
||||
|
||||
func B2s(b []byte) string {
|
||||
@@ -86,3 +100,26 @@ func B2s(b []byte) string {
|
||||
func S2b(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
|
||||
func init() {
|
||||
defer func() {
|
||||
rec := recover()
|
||||
if rec != nil {
|
||||
log.Printf("WARNING: failed to parse repo and commit, please report this as an issue!!! repo: %s commit: %s\n", Repo, Commit)
|
||||
}
|
||||
}()
|
||||
|
||||
if Repo != "unknown" {
|
||||
if !strings.HasPrefix(Repo, "http") {
|
||||
s := strings.Split(Repo, "@")
|
||||
s = strings.Split(s[1], ":")
|
||||
CommitURL = "https://" + s[0] + "/" + s[1]
|
||||
} else {
|
||||
CommitURL = strings.TrimSuffix(Repo, "/")
|
||||
}
|
||||
|
||||
CommitURL = strings.TrimSuffix(CommitURL, ".git")
|
||||
|
||||
CommitURL += "/commit/" + Commit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package misc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
@@ -41,7 +41,7 @@ func (pr *ProxyReader) Close() error {
|
||||
|
||||
func Log(what ...any) {
|
||||
if cfg.Debug {
|
||||
fmt.Println(what...)
|
||||
log.Println(what...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
const on = "on"
|
||||
|
||||
func Defaults(dst *cfg.Preferences) {
|
||||
if dst.Player == nil {
|
||||
dst.Player = cfg.DefaultPreferences.Player
|
||||
@@ -36,6 +38,10 @@ func Defaults(dst *cfg.Preferences) {
|
||||
dst.AutoplayNextTrack = cfg.DefaultPreferences.AutoplayNextTrack
|
||||
}
|
||||
|
||||
if dst.AutoplayNextRelatedTrack == nil {
|
||||
dst.AutoplayNextRelatedTrack = cfg.DefaultPreferences.AutoplayNextRelatedTrack
|
||||
}
|
||||
|
||||
if dst.DefaultAutoplayMode == nil {
|
||||
dst.DefaultAutoplayMode = cfg.DefaultPreferences.DefaultAutoplayMode
|
||||
}
|
||||
@@ -63,6 +69,10 @@ func Defaults(dst *cfg.Preferences) {
|
||||
if dst.DynamicLoadComments == nil {
|
||||
dst.DynamicLoadComments = cfg.DefaultPreferences.DynamicLoadComments
|
||||
}
|
||||
|
||||
if dst.KeepPlayerFocus == nil {
|
||||
dst.KeepPlayerFocus = cfg.DefaultPreferences.KeepPlayerFocus
|
||||
}
|
||||
}
|
||||
|
||||
func Get(c fiber.Ctx) (cfg.Preferences, error) {
|
||||
@@ -79,19 +89,21 @@ func Get(c fiber.Ctx) (cfg.Preferences, error) {
|
||||
}
|
||||
|
||||
type PrefsForm struct {
|
||||
ProxyImages string
|
||||
ParseDescriptions string
|
||||
Player string
|
||||
ProxyStreams string
|
||||
FullyPreloadTrack string
|
||||
AutoplayNextTrack string
|
||||
DefaultAutoplayMode string
|
||||
HLSAudio string
|
||||
RestreamAudio string
|
||||
DownloadAudio string
|
||||
ShowAudio string
|
||||
SearchSuggestions string
|
||||
DynamicLoadComments string
|
||||
ProxyImages string
|
||||
ParseDescriptions string
|
||||
Player string
|
||||
ProxyStreams string
|
||||
FullyPreloadTrack string
|
||||
AutoplayNextTrack string
|
||||
AutoplayNextRelatedTrack string
|
||||
DefaultAutoplayMode string
|
||||
HLSAudio string
|
||||
RestreamAudio string
|
||||
DownloadAudio string
|
||||
ShowAudio string
|
||||
SearchSuggestions string
|
||||
DynamicLoadComments string
|
||||
KeepPlayerFocus string
|
||||
}
|
||||
|
||||
type Export struct {
|
||||
@@ -105,7 +117,7 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
c.Response().Header.SetContentType("text/html")
|
||||
return templates.Base("preferences", templates.Preferences(p), nil).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
@@ -125,13 +137,19 @@ func Load(r *fiber.App) {
|
||||
old.DefaultAutoplayMode = &p.DefaultAutoplayMode
|
||||
}
|
||||
|
||||
if p.AutoplayNextTrack == "on" {
|
||||
if p.AutoplayNextTrack == on {
|
||||
old.AutoplayNextTrack = &cfg.True
|
||||
} else {
|
||||
old.AutoplayNextTrack = &cfg.False
|
||||
}
|
||||
|
||||
if p.ShowAudio == "on" {
|
||||
if p.AutoplayNextRelatedTrack == on {
|
||||
old.AutoplayNextRelatedTrack = &cfg.True
|
||||
} else {
|
||||
old.AutoplayNextRelatedTrack = &cfg.False
|
||||
}
|
||||
|
||||
if p.ShowAudio == on {
|
||||
old.ShowAudio = &cfg.True
|
||||
} else {
|
||||
old.ShowAudio = &cfg.False
|
||||
@@ -139,14 +157,14 @@ func Load(r *fiber.App) {
|
||||
|
||||
if *old.Player == cfg.HLSPlayer {
|
||||
if cfg.ProxyStreams {
|
||||
if p.ProxyStreams == "on" {
|
||||
if p.ProxyStreams == on {
|
||||
old.ProxyStreams = &cfg.True
|
||||
} else if p.ProxyStreams == "" {
|
||||
old.ProxyStreams = &cfg.False
|
||||
}
|
||||
}
|
||||
|
||||
if p.FullyPreloadTrack == "on" {
|
||||
if p.FullyPreloadTrack == on {
|
||||
old.FullyPreloadTrack = &cfg.True
|
||||
} else if p.FullyPreloadTrack == "" {
|
||||
old.FullyPreloadTrack = &cfg.False
|
||||
@@ -164,31 +182,37 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
|
||||
if cfg.ProxyImages {
|
||||
if p.ProxyImages == "on" {
|
||||
if p.ProxyImages == on {
|
||||
old.ProxyImages = &cfg.True
|
||||
} else if p.ProxyImages == "" {
|
||||
old.ProxyImages = &cfg.False
|
||||
}
|
||||
}
|
||||
|
||||
if p.ParseDescriptions == "on" {
|
||||
if p.ParseDescriptions == on {
|
||||
old.ParseDescriptions = &cfg.True
|
||||
} else {
|
||||
old.ParseDescriptions = &cfg.False
|
||||
}
|
||||
|
||||
if p.SearchSuggestions == "on" {
|
||||
if p.SearchSuggestions == on {
|
||||
old.SearchSuggestions = &cfg.True
|
||||
} else {
|
||||
old.SearchSuggestions = &cfg.False
|
||||
}
|
||||
|
||||
if p.DynamicLoadComments == "on" {
|
||||
if p.DynamicLoadComments == on {
|
||||
old.DynamicLoadComments = &cfg.True
|
||||
} else {
|
||||
old.DynamicLoadComments = &cfg.False
|
||||
}
|
||||
|
||||
if p.KeepPlayerFocus == on {
|
||||
old.KeepPlayerFocus = &cfg.True
|
||||
} else {
|
||||
old.KeepPlayerFocus = &cfg.False
|
||||
}
|
||||
|
||||
old.Player = &p.Player
|
||||
|
||||
data, err := json.Marshal(old)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
var al_httpc *fasthttp.HostClient
|
||||
var sndcdn = []byte(".sndcdn.com")
|
||||
|
||||
func Load(r *fiber.App) {
|
||||
|
||||
@@ -36,7 +37,7 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), []byte(".sndcdn.com")) {
|
||||
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -53,7 +54,6 @@ func Load(r *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
//req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") images not big enough to be compressed
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
//defer fasthttp.ReleaseResponse(resp) moved to proxyreader!!!
|
||||
@@ -63,12 +63,12 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "image/jpeg")
|
||||
c.Response().Header.SetContentTypeBytes(resp.Header.ContentType())
|
||||
c.Set("Cache-Control", cfg.ImageCacheControl)
|
||||
//return c.Send(resp.Body())
|
||||
pr := misc.AcquireProxyReader()
|
||||
pr.Reader = resp.BodyStream()
|
||||
pr.Resp = resp
|
||||
return c.SendStream(pr)
|
||||
return c.SendStream(pr, resp.Header.ContentLength())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var sndcdn = []byte(".sndcdn.com")
|
||||
var soundcloudcloud = []byte(".soundcloud.cloud")
|
||||
var newline = []byte{'\n'}
|
||||
var extxmap = []byte(`#EXT-X-MAP:URI="`)
|
||||
|
||||
func Load(a *fiber.App) {
|
||||
r := a.Group("/_/proxy/streams")
|
||||
|
||||
@@ -28,7 +33,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), []byte(".sndcdn.com")) {
|
||||
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -37,7 +42,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
//defer fasthttp.ReleaseResponse(resp)
|
||||
@@ -67,7 +71,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), []byte(".soundcloud.cloud")) {
|
||||
if !bytes.HasSuffix(parsed.Host(), soundcloudcloud) {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -76,7 +80,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
@@ -105,7 +108,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), []byte(".sndcdn.com")) {
|
||||
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -114,7 +117,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
@@ -129,7 +131,7 @@ func Load(a *fiber.App) {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
var sp = bytes.Split(data, []byte{'\n'})
|
||||
var sp = bytes.Split(data, newline)
|
||||
for i, l := range sp {
|
||||
if len(l) == 0 || l[0] == '#' {
|
||||
continue
|
||||
@@ -139,7 +141,7 @@ func Load(a *fiber.App) {
|
||||
sp[i] = l
|
||||
}
|
||||
|
||||
return c.Send(bytes.Join(sp, []byte("\n")))
|
||||
return c.Send(bytes.Join(sp, newline))
|
||||
})
|
||||
|
||||
r.Get("/playlist/aac", func(c fiber.Ctx) error {
|
||||
@@ -156,7 +158,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), []byte(".soundcloud.cloud")) {
|
||||
if !bytes.HasSuffix(parsed.Host(), soundcloudcloud) {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -165,7 +167,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
@@ -175,19 +176,14 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
var sp = bytes.Split(data, []byte("\n"))
|
||||
var sp = bytes.Split(resp.Body(), newline)
|
||||
for i, l := range sp {
|
||||
if len(l) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if l[0] == '#' {
|
||||
if bytes.HasPrefix(l, []byte(`#EXT-X-MAP:URI="`)) {
|
||||
if bytes.HasPrefix(l, extxmap) {
|
||||
l = []byte(`#EXT-X-MAP:URI="/_/proxy/streams/aac?url=` + url.QueryEscape(cfg.B2s(l[16:len(l)-1])) + `"`)
|
||||
sp[i] = l
|
||||
}
|
||||
@@ -199,6 +195,6 @@ func Load(a *fiber.App) {
|
||||
sp[i] = l
|
||||
}
|
||||
|
||||
return c.Send(bytes.Join(sp, []byte("\n")))
|
||||
return c.Send(bytes.Join(sp, newline))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package restream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"sync"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/misc"
|
||||
@@ -17,171 +19,6 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const defaultPartsCapacity = 24
|
||||
|
||||
type reader struct {
|
||||
parts [][]byte
|
||||
leftover []byte
|
||||
index int
|
||||
|
||||
req *fasthttp.Request
|
||||
resp *fasthttp.Response
|
||||
client *fasthttp.HostClient
|
||||
}
|
||||
|
||||
var readerpool = sync.Pool{
|
||||
New: func() any {
|
||||
return &reader{}
|
||||
},
|
||||
}
|
||||
|
||||
func acquireReader() *reader {
|
||||
return readerpool.Get().(*reader)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
r.req.SetRequestURI(url)
|
||||
r.req.Header.SetUserAgent(cfg.UserAgent)
|
||||
r.req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
if aac {
|
||||
r.client = misc.HlsAacClient
|
||||
} else {
|
||||
r.client = misc.HlsClient
|
||||
}
|
||||
|
||||
err := sc.DoWithRetry(r.client, r.req, r.resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := r.resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = r.resp.Body()
|
||||
}
|
||||
|
||||
if r.parts == nil {
|
||||
misc.Log("make() r.parts")
|
||||
r.parts = make([][]byte, 0, defaultPartsCapacity)
|
||||
} else {
|
||||
misc.Log(cap(r.parts), len(r.parts))
|
||||
}
|
||||
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]))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (r *reader) Close() error {
|
||||
misc.Log("closed :D")
|
||||
if r.req != nil {
|
||||
fasthttp.ReleaseRequest(r.req)
|
||||
r.req = nil
|
||||
}
|
||||
|
||||
if r.resp != nil {
|
||||
fasthttp.ReleaseResponse(r.resp)
|
||||
r.resp = nil
|
||||
}
|
||||
|
||||
r.client = nil
|
||||
r.leftover = nil
|
||||
r.index = 0
|
||||
r.parts = r.parts[:0]
|
||||
|
||||
readerpool.Put(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// you could prob make this a bit faster by concurrency (make a bunch of workers => make them download the parts => temporarily add them to a map => fully assemble the result => make reader.Read() read out the result as the parts are coming in) but whatever, fine for now
|
||||
func (r *reader) Read(buf []byte) (n int, err error) {
|
||||
misc.Log("we read")
|
||||
if len(r.leftover) != 0 {
|
||||
h := len(buf)
|
||||
if h > len(r.leftover) {
|
||||
h = len(r.leftover)
|
||||
}
|
||||
|
||||
n = copy(buf, r.leftover[:h])
|
||||
|
||||
if n > len(r.leftover) {
|
||||
r.leftover = r.leftover[:0]
|
||||
} else {
|
||||
r.leftover = r.leftover[n:]
|
||||
}
|
||||
|
||||
if n < len(buf) && r.index == len(r.parts) {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.index == len(r.parts) {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
r.req.SetRequestURIBytes(r.parts[r.index])
|
||||
|
||||
err = sc.DoWithRetry(r.client, r.req, r.resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := r.resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = r.resp.Body()
|
||||
}
|
||||
|
||||
if len(data) > len(buf) {
|
||||
n = copy(buf, data[:len(buf)])
|
||||
} else {
|
||||
n = copy(buf, data)
|
||||
}
|
||||
|
||||
r.leftover = data[n:]
|
||||
r.index++
|
||||
|
||||
if n < len(buf) && r.index == len(r.parts) {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type collector struct {
|
||||
data []byte
|
||||
}
|
||||
@@ -211,14 +48,19 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
|
||||
var isDownload = string(c.RequestCtx().QueryArgs().Peek("metadata")) == "true"
|
||||
var quality *string
|
||||
if isDownload {
|
||||
quality = p.DownloadAudio
|
||||
var forcedQuality = c.RequestCtx().QueryArgs().Peek("audio")
|
||||
var quality string
|
||||
if len(forcedQuality) != 0 {
|
||||
quality = cfg.B2s(forcedQuality)
|
||||
} else {
|
||||
quality = p.RestreamAudio
|
||||
if isDownload {
|
||||
quality = *p.DownloadAudio
|
||||
} else {
|
||||
quality = *p.RestreamAudio
|
||||
}
|
||||
}
|
||||
|
||||
tr, audio := t.Media.SelectCompatible(*quality, true)
|
||||
tr, audio := t.Media.SelectCompatible(quality, true)
|
||||
if tr == nil {
|
||||
return fiber.ErrExpectationFailed
|
||||
}
|
||||
@@ -228,14 +70,19 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", tr.Format.MimeType)
|
||||
c.Response().Header.SetContentType(tr.Format.MimeType)
|
||||
c.Set("Cache-Control", cfg.RestreamCacheControl)
|
||||
|
||||
if isDownload {
|
||||
if t.Artwork != "" {
|
||||
t.Artwork = strings.Replace(t.Artwork, "t500x500", "original", 1)
|
||||
}
|
||||
|
||||
switch audio {
|
||||
case cfg.AudioMP3:
|
||||
r := acquireReader()
|
||||
if err := r.Setup(u, false); err != nil {
|
||||
err := r.Setup(u, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -249,156 +96,55 @@ func Load(r *fiber.App) {
|
||||
tag.SetTitle(t.Title)
|
||||
|
||||
if t.Artwork != "" {
|
||||
data, mime, err := t.DownloadImage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.req.SetRequestURI(t.Artwork)
|
||||
|
||||
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: mime, Picture: data, PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
|
||||
err := sc.DoWithRetry(misc.ImageClient, r.req, r.resp)
|
||||
if err == nil && r.resp.StatusCode() == 200 {
|
||||
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: cfg.B2s(r.req.Header.ContentType()), Picture: r.req.Body(), 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:
|
||||
case cfg.AudioOpus: // might try to fuck around with metadata injection. Dynamically injecting metadata for opus wasn't really good idea as it breaks some things :P
|
||||
req := fasthttp.AcquireRequest()
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.SetRequestURI(u)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err = sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
err := sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
parts := make([][]byte, 0, defaultPartsCapacity)
|
||||
for _, s := range bytes.Split(data, []byte{'\n'}) {
|
||||
for _, s := range bytes.Split(resp.Body(), newline) {
|
||||
if len(s) == 0 || s[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, s)
|
||||
}
|
||||
|
||||
result := []byte{}
|
||||
|
||||
for _, part := range parts {
|
||||
req.SetRequestURIBytes(part)
|
||||
|
||||
err = sc.DoWithRetry(misc.HlsClient, 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(misc.ImageClient, 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.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err = sc.DoWithRetry(misc.HlsAacClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
parts := make([][]byte, 0, defaultPartsCapacity)
|
||||
// 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(misc.HlsAacClient, req, resp)
|
||||
res := make([]byte, 0, 1024*1024*1)
|
||||
for _, s := range parts {
|
||||
req.SetRequestURIBytes(s)
|
||||
err := sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
result = append(result, data...)
|
||||
res = append(res, resp.Body()...)
|
||||
}
|
||||
|
||||
tag, err := mp4meta.ReadMP4(bytes.NewReader(result))
|
||||
tag, err := oggmeta.ReadOGG(bytes.NewReader(res))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -412,28 +158,73 @@ func Load(r *fiber.App) {
|
||||
|
||||
if t.Artwork != "" {
|
||||
req.SetRequestURI(t.Artwork)
|
||||
req.Header.Del("Accept-Encoding")
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
if err == nil && resp.StatusCode() == 200 {
|
||||
parsed, _, err := image.Decode(resp.BodyStream())
|
||||
if err == nil {
|
||||
tag.SetCoverArt(&parsed)
|
||||
}
|
||||
}
|
||||
|
||||
defer resp.CloseBodyStream()
|
||||
parsed, err := jpeg.Decode(resp.BodyStream())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag.SetCoverArt(&parsed)
|
||||
}
|
||||
|
||||
return tag.Save(c)
|
||||
return tag.Save(c.Response().BodyWriter())
|
||||
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(misc.ImageClient, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var col collector
|
||||
tag.Save(&col)
|
||||
fixDuration(col.data, &t.Duration)
|
||||
r.leftover = col.data
|
||||
|
||||
return c.SendStream(r)
|
||||
}
|
||||
}
|
||||
|
||||
r := acquireReader()
|
||||
if err := r.Setup(u, audio == cfg.AudioAAC); err != nil {
|
||||
if audio == cfg.AudioAAC {
|
||||
err = r.Setup(u, true, &t.Duration)
|
||||
} else {
|
||||
err = r.Setup(u, false, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
186
lib/restream/reader.go
Normal file
186
lib/restream/reader.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package restream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/misc"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const defaultPartsCapacity = 24
|
||||
|
||||
type reader struct {
|
||||
duration *uint32
|
||||
|
||||
req *fasthttp.Request
|
||||
resp *fasthttp.Response
|
||||
client *fasthttp.HostClient
|
||||
parts [][]byte
|
||||
leftover []byte
|
||||
index int
|
||||
}
|
||||
|
||||
var readerpool = sync.Pool{
|
||||
New: func() any {
|
||||
return &reader{}
|
||||
},
|
||||
}
|
||||
|
||||
func acquireReader() *reader {
|
||||
return readerpool.Get().(*reader)
|
||||
}
|
||||
|
||||
func clone(buf []byte) []byte {
|
||||
out := make([]byte, len(buf))
|
||||
copy(out, buf)
|
||||
return out
|
||||
}
|
||||
|
||||
var mvhd = []byte("mvhd")
|
||||
var newline = []byte{'\n'}
|
||||
|
||||
func fixDuration(data []byte, duration *uint32) {
|
||||
i := bytes.Index(data, mvhd)
|
||||
if i != -1 {
|
||||
i += 20
|
||||
|
||||
bt := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bt, *duration)
|
||||
copy(data[i:], bt)
|
||||
// timescale is already 1000 in the files
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reader) Setup(url string, aac bool, duration *uint32) error {
|
||||
if r.req == nil {
|
||||
r.req = fasthttp.AcquireRequest()
|
||||
}
|
||||
|
||||
if r.resp == nil {
|
||||
r.resp = fasthttp.AcquireResponse()
|
||||
}
|
||||
|
||||
r.req.SetRequestURI(url)
|
||||
r.req.Header.SetUserAgent(cfg.UserAgent)
|
||||
|
||||
if aac {
|
||||
r.client = misc.HlsAacClient
|
||||
r.duration = duration
|
||||
} else {
|
||||
r.client = misc.HlsClient
|
||||
}
|
||||
|
||||
err := sc.DoWithRetry(r.client, r.req, r.resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.parts == nil {
|
||||
misc.Log("make() r.parts")
|
||||
r.parts = make([][]byte, 0, defaultPartsCapacity)
|
||||
} else {
|
||||
misc.Log(cap(r.parts), len(r.parts))
|
||||
}
|
||||
// clone needed to mitigate memory skill issues smh
|
||||
if aac {
|
||||
for _, s := range bytes.Split(r.resp.Body(), newline) {
|
||||
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]))
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
r.parts = append(r.parts, clone(s))
|
||||
}
|
||||
} else {
|
||||
for _, s := range bytes.Split(r.resp.Body(), newline) {
|
||||
if len(s) == 0 || s[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
r.parts = append(r.parts, clone(s))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reader) Close() error {
|
||||
misc.Log("closed :D")
|
||||
r.req.Reset()
|
||||
r.resp.Reset()
|
||||
|
||||
r.leftover = nil
|
||||
r.index = 0
|
||||
r.parts = r.parts[:0]
|
||||
|
||||
readerpool.Put(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// I have no idea what this truly even does anymore. Maybe a rewrite/refactor would be good?
|
||||
func (r *reader) Read(buf []byte) (n int, err error) {
|
||||
misc.Log("we read")
|
||||
if len(r.leftover) != 0 {
|
||||
h := len(buf)
|
||||
if h > len(r.leftover) {
|
||||
h = len(r.leftover)
|
||||
}
|
||||
|
||||
n = copy(buf, r.leftover[:h])
|
||||
|
||||
if n > len(r.leftover) {
|
||||
r.leftover = r.leftover[:0]
|
||||
} else {
|
||||
r.leftover = r.leftover[n:]
|
||||
}
|
||||
|
||||
if n < len(buf) && r.index == len(r.parts) {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.index == len(r.parts) {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
r.req.SetRequestURIBytes(r.parts[r.index])
|
||||
|
||||
err = sc.DoWithRetry(r.client, r.req, r.resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := r.resp.Body()
|
||||
if r.index == 0 && r.duration != nil {
|
||||
fixDuration(data, r.duration) // I'm guessing that mvhd will always be in first part
|
||||
}
|
||||
|
||||
if len(data) > len(buf) {
|
||||
n = copy(buf, data[:len(buf)])
|
||||
} else {
|
||||
n = copy(buf, data)
|
||||
}
|
||||
|
||||
r.leftover = data[n:]
|
||||
r.index++
|
||||
|
||||
if n < len(buf) && r.index == len(r.parts) {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,31 +1,65 @@
|
||||
package sc
|
||||
|
||||
import "git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
// Functions/structions related to featured/suggested content
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
)
|
||||
|
||||
type Selection struct {
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"` // should always be "selection"!
|
||||
Items Paginated[*Playlist] `json:"items"` // ?? why
|
||||
// Functions/structures related to featured/suggested content
|
||||
|
||||
type PlaylistOrUser struct {
|
||||
Kind string `json:"kind"` // "playlist" or "system-playlist" or "user"
|
||||
Permalink string `json:"permalink"`
|
||||
|
||||
// User-specific
|
||||
Avatar string `json:"avatar_url"`
|
||||
Username string `json:"username"`
|
||||
FullName string `json:"full_name"`
|
||||
|
||||
// Playlist-specific
|
||||
Title string `json:"title"`
|
||||
Author struct {
|
||||
Permalink string `string:"permalink"`
|
||||
} `json:"user"`
|
||||
Artwork string `json:"artwork_url"`
|
||||
TrackCount int64 `json:"track_count"`
|
||||
}
|
||||
|
||||
func GetFeaturedTracks(cid string, prefs cfg.Preferences, args string) (*Paginated[*Track], error) {
|
||||
p := Paginated[*Track]{Next: "https://" + api + "/featured_tracks/top/all-music" + args}
|
||||
// DO NOT UNFOLD
|
||||
// dangerous
|
||||
// seems to go in an infinite loop
|
||||
err := p.Proceed(cid, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (p PlaylistOrUser) Href() string {
|
||||
switch p.Kind {
|
||||
case "system-playlist":
|
||||
return "/discover/sets/" + p.Permalink
|
||||
case "playlist":
|
||||
return "/" + p.Author.Permalink + "/sets/" + p.Permalink
|
||||
default:
|
||||
return "/" + p.Permalink
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range p.Collection {
|
||||
t.Fix(false, false)
|
||||
t.Postfix(prefs, false)
|
||||
func (p *PlaylistOrUser) Fix(prefs cfg.Preferences) {
|
||||
switch p.Kind {
|
||||
case "user":
|
||||
if p.Avatar == "https://a1.sndcdn.com/images/default_avatar_large.png" {
|
||||
p.Avatar = ""
|
||||
} else {
|
||||
p.Avatar = strings.Replace(p.Avatar, "-large.", "-t200x200.", 1)
|
||||
}
|
||||
default:
|
||||
if p.Artwork != "" {
|
||||
p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1)
|
||||
if cfg.ProxyImages && *prefs.ProxyImages {
|
||||
p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
type Selection struct {
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"` // should always be "selection"!
|
||||
Items Paginated[*PlaylistOrUser] `json:"items"` // ?? why
|
||||
}
|
||||
|
||||
func GetSelections(cid string, prefs cfg.Preferences) (*Paginated[*Selection], error) {
|
||||
@@ -45,7 +79,6 @@ func GetSelections(cid string, prefs cfg.Preferences) (*Paginated[*Selection], e
|
||||
|
||||
func (s *Selection) Fix(prefs cfg.Preferences) {
|
||||
for _, p := range s.Items.Collection {
|
||||
p.Fix("", false, false)
|
||||
p.Postfix(prefs, false, false)
|
||||
p.Fix(prefs)
|
||||
}
|
||||
}
|
||||
|
||||
168
lib/sc/init.go
168
lib/sc/init.go
@@ -1,6 +1,7 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -17,9 +18,9 @@ import (
|
||||
)
|
||||
|
||||
type clientIdCache struct {
|
||||
NextCheck time.Time
|
||||
ClientID string
|
||||
Version string
|
||||
NextCheck time.Time
|
||||
}
|
||||
|
||||
var ClientIDCache clientIdCache
|
||||
@@ -28,6 +29,11 @@ const api = "api-v2.soundcloud.com"
|
||||
|
||||
const H = len("https://" + api)
|
||||
|
||||
var newline = []byte("\n")
|
||||
|
||||
var sc_version = []byte(`<script>window.__sc_version="`)
|
||||
var script0 = []byte(`<script crossorigin src="https://a-v2.sndcdn.com/assets/0-`)
|
||||
var script = []byte(`<script crossorigin src="https://a-v2.sndcdn.com/assets/`)
|
||||
var httpc = &fasthttp.HostClient{
|
||||
Addr: api + ":443",
|
||||
IsTLS: true,
|
||||
@@ -39,11 +45,12 @@ var genericClient = &fasthttp.Client{
|
||||
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
|
||||
}
|
||||
|
||||
// var verRegex = regexp2.MustCompile(`^<script>window\.__sc_version="([0-9]{10})"</script>$`, 2)
|
||||
// var scriptsRegex = regexp2.MustCompile(`^<script crossorigin src="(https://a-v2\.sndcdn\.com/assets/.+\.js)"></script>$`, 2)
|
||||
// var scriptRegex = regexp2.MustCompile(`^<script crossorigin src="(https://a-v2\.sndcdn\.com/assets/0-.+\.js)"></script>$`, 2)
|
||||
|
||||
//go:generate regexp2cg -package sc -o regexp2_codegen.go
|
||||
var verRegex = regexp2.MustCompile(`^<script>window\.__sc_version="([0-9]{10})"</script>$`, 2)
|
||||
var scriptsRegex = regexp2.MustCompile(`^<script crossorigin src="(https://a-v2\.sndcdn\.com/assets/.+\.js)"></script>$`, 2)
|
||||
var scriptRegex = regexp2.MustCompile(`^<script crossorigin src="(https://a-v2\.sndcdn\.com/assets/0-.+\.js)"></script>$`, 2)
|
||||
var clientIdRegex = regexp2.MustCompile(`\("client_id=([A-Za-z0-9]{32})"\)`, 0)
|
||||
var clientIdRegex = regexp2.MustCompile(`client_id:"([A-Za-z0-9]{32})"`, 0) //regexp2.MustCompile(`\("client_id=([A-Za-z0-9]{32})"\)`, 0)
|
||||
var ErrVersionNotFound = errors.New("version not found")
|
||||
var ErrScriptNotFound = errors.New("script not found")
|
||||
var ErrIDNotFound = errors.New("clientid not found")
|
||||
@@ -55,12 +62,12 @@ type cached[T any] struct {
|
||||
}
|
||||
|
||||
// don't be spooked by misc.Log, it will be removed during compilation if cfg.Debug == false
|
||||
func processFile(wg *sync.WaitGroup, ch chan string, uri string, isDone *bool) {
|
||||
misc.Log(uri)
|
||||
func processFile(wg *sync.WaitGroup, ch chan string, uri []byte, isDone *bool) {
|
||||
misc.Log(string(uri))
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI(uri)
|
||||
req.SetRequestURIBytes(uri)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
@@ -105,19 +112,23 @@ func processFile(wg *sync.WaitGroup, ch chan string, uri string, isDone *bool) {
|
||||
}
|
||||
|
||||
ch <- g.String()
|
||||
misc.Log("found in", uri)
|
||||
misc.Log("found in", string(uri))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
misc.Log("not found in", uri)
|
||||
misc.Log("not found in", string(uri))
|
||||
}
|
||||
|
||||
// Experimental method, which asserts that the clientId is inside the file that starts with "0-"
|
||||
const experimental_GetClientID = true
|
||||
|
||||
// inspired by github.com/imputnet/cobalt (mostly stolen lol)
|
||||
// inspired by github.com/imputnet/cobalt
|
||||
func GetClientID() (string, error) {
|
||||
if cfg.ClientID != "" {
|
||||
return cfg.ClientID, nil
|
||||
}
|
||||
|
||||
if ClientIDCache.NextCheck.After(time.Now()) {
|
||||
misc.Log("clientidcache hit @ 1")
|
||||
return ClientIDCache.ClientID, nil
|
||||
@@ -143,65 +154,82 @@ func GetClientID() (string, error) {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
m, _ := verRegex.FindStringMatch(cfg.B2s(data))
|
||||
if m == nil {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
|
||||
g := m.GroupByNumber(1)
|
||||
if g == nil {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
|
||||
ver := g.String()
|
||||
if ver == ClientIDCache.Version {
|
||||
misc.Log("clientidcache hit @ ver")
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
|
||||
if experimental_GetClientID {
|
||||
m, _ = scriptRegex.FindStringMatch(cfg.B2s(data))
|
||||
var ver string
|
||||
var scriptUrl []byte
|
||||
for l := range bytes.SplitSeq(data, newline) { // version usually comes earlier, but retest this sometimes !!!
|
||||
if ver == "" && bytes.HasPrefix(l, sc_version) {
|
||||
ver = cfg.B2s(l[len(sc_version) : len(l)-len(`"</script>`)])
|
||||
misc.Log("found ver:", ver)
|
||||
if ClientIDCache.Version != "" && ver == ClientIDCache.Version {
|
||||
goto verCacheHit
|
||||
}
|
||||
} else if bytes.HasPrefix(l, script0) {
|
||||
scriptUrl = l[len(`<script crossorigin src="`) : len(l)-len(`"></script>`)]
|
||||
misc.Log("found scriptUrl:", string(scriptUrl))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ver == "" {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
|
||||
if scriptUrl == nil {
|
||||
return "", ErrScriptNotFound
|
||||
}
|
||||
|
||||
req.SetRequestURIBytes(scriptUrl)
|
||||
err = DoWithRetryAll(genericClient, req, resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err = resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
m, _ := clientIdRegex.FindStringMatch(cfg.B2s(data))
|
||||
if m != nil {
|
||||
g = m.GroupByNumber(1)
|
||||
g := m.GroupByNumber(1)
|
||||
if g != nil {
|
||||
req.SetRequestURI(g.String())
|
||||
err = DoWithRetryAll(genericClient, req, resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err = resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
m, _ = clientIdRegex.FindStringMatch(cfg.B2s(data))
|
||||
if m != nil {
|
||||
g = m.GroupByNumber(1)
|
||||
if g != nil {
|
||||
ClientIDCache.ClientID = g.String()
|
||||
ClientIDCache.Version = ver
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
misc.Log(ClientIDCache)
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
}
|
||||
ClientIDCache.ClientID = g.String()
|
||||
ClientIDCache.Version = ver
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
misc.Log(ClientIDCache)
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ch := make(chan string, 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
done := false
|
||||
m, _ = scriptsRegex.FindStringMatch(cfg.B2s(data))
|
||||
for m != nil {
|
||||
g = m.GroupByNumber(1)
|
||||
if g != nil {
|
||||
wg.Add(1)
|
||||
go processFile(wg, ch, g.String(), &done)
|
||||
}
|
||||
|
||||
m, _ = scriptsRegex.FindNextMatch(m)
|
||||
var ver string
|
||||
var scriptUrls = make([][]byte, 0, 10) // Usually only 8 chunks, so I went with jsut a bit more to be safe
|
||||
for l := range bytes.SplitSeq(data, newline) {
|
||||
if ver == "" && bytes.HasPrefix(l, sc_version) {
|
||||
ver = cfg.B2s(l[len(sc_version) : len(l)-len(`"</script>`)])
|
||||
if ver == ClientIDCache.Version {
|
||||
goto verCacheHit
|
||||
}
|
||||
} else if bytes.HasPrefix(l, script) {
|
||||
scriptUrls = append(scriptUrls, l[len(`<script crossorigin src="`):len(l)-len(`"></script>`)])
|
||||
}
|
||||
}
|
||||
|
||||
if ver == "" {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
|
||||
if len(scriptUrls) == 0 {
|
||||
return "", ErrScriptNotFound
|
||||
}
|
||||
|
||||
for _, s := range scriptUrls {
|
||||
wg.Add(1)
|
||||
go processFile(wg, ch, s, &done)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -234,11 +262,16 @@ func GetClientID() (string, error) {
|
||||
}
|
||||
|
||||
return "", ErrIDNotFound
|
||||
|
||||
verCacheHit:
|
||||
misc.Log("clientidcache hit @ ver")
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
|
||||
// Just retry any kind of errors, why not
|
||||
func DoWithRetryAll(httpc *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) (err error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
err = httpc.Do(req, resp)
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -250,7 +283,7 @@ func DoWithRetryAll(httpc *fasthttp.Client, req *fasthttp.Request, resp *fasthtt
|
||||
|
||||
// Since the http client is setup to always keep connections idle (great for speed, no need to open a new one everytime), those connections may be closed by soundcloud after some time of inactivity, this ensures that we retry those requests that fail due to the connection closing/timing out
|
||||
func DoWithRetry(httpc *fasthttp.HostClient, req *fasthttp.Request, resp *fasthttp.Response) (err error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
err = httpc.Do(req, resp)
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -309,9 +342,9 @@ func Resolve(cid string, path string, out any) error {
|
||||
}
|
||||
|
||||
type Paginated[T any] struct {
|
||||
Next string `json:"next_href"`
|
||||
Collection []T `json:"collection"`
|
||||
Total int64 `json:"total_results"`
|
||||
Next string `json:"next_href"`
|
||||
}
|
||||
|
||||
func (p *Paginated[T]) Proceed(cid string, shouldUnfold bool) error {
|
||||
@@ -426,8 +459,9 @@ func init() {
|
||||
for range ticker.C {
|
||||
usersCacheLock.Lock()
|
||||
|
||||
now := time.Now()
|
||||
for key, val := range UsersCache {
|
||||
if val.Expires.Before(time.Now()) {
|
||||
if val.Expires.Before(now) {
|
||||
delete(UsersCache, key)
|
||||
}
|
||||
}
|
||||
@@ -441,8 +475,9 @@ func init() {
|
||||
for range ticker.C {
|
||||
tracksCacheLock.Lock()
|
||||
|
||||
now := time.Now()
|
||||
for key, val := range TracksCache {
|
||||
if val.Expires.Before(time.Now()) {
|
||||
if val.Expires.Before(now) {
|
||||
delete(TracksCache, key)
|
||||
}
|
||||
}
|
||||
@@ -456,8 +491,9 @@ func init() {
|
||||
for range ticker.C {
|
||||
playlistsCacheLock.Lock()
|
||||
|
||||
now := time.Now()
|
||||
for key, val := range PlaylistsCache {
|
||||
if val.Expires.Before(time.Now()) {
|
||||
if val.Expires.Before(now) {
|
||||
delete(PlaylistsCache, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,28 +16,26 @@ var playlistsCacheLock = &sync.RWMutex{}
|
||||
// Functions/structures related to playlists
|
||||
|
||||
type Playlist struct {
|
||||
Artwork string `json:"artwork_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
Kind string `json:"kind"` // should always be "playlist"! or "system-playlist"
|
||||
LastModified string `json:"last_modified"`
|
||||
Likes int64 `json:"likes_count"`
|
||||
Permalink string `json:"permalink"`
|
||||
//ReleaseDate string `json:"release_date"`
|
||||
TagList string `json:"tag_list"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"set_type"`
|
||||
Album bool `json:"is_album"`
|
||||
Author User `json:"user"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
TrackCount int64 `json:"track_count"`
|
||||
|
||||
MissingTracks string `json:"-"`
|
||||
Artwork string `json:"artwork_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
Kind string `json:"kind"` // should always be "playlist"! or "system-playlist"
|
||||
LastModified string `json:"last_modified"`
|
||||
Permalink string `json:"permalink"`
|
||||
TagList string `json:"tag_list"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"set_type"`
|
||||
MissingTracks string `json:"-"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
Author User `json:"user"`
|
||||
Likes int64 `json:"likes_count"`
|
||||
TrackCount int64 `json:"track_count"`
|
||||
Album bool `json:"is_album"`
|
||||
}
|
||||
|
||||
func GetPlaylist(cid string, permalink string) (Playlist, error) {
|
||||
playlistsCacheLock.RLock()
|
||||
if cell, ok := PlaylistsCache[permalink]; ok && cell.Expires.After(time.Now()) {
|
||||
if cell, ok := PlaylistsCache[permalink]; ok {
|
||||
playlistsCacheLock.RUnlock()
|
||||
return cell.Value, nil
|
||||
}
|
||||
|
||||
@@ -24,42 +24,46 @@ var TracksCache = map[string]cached[Track]{}
|
||||
var tracksCacheLock = &sync.RWMutex{}
|
||||
|
||||
type Track struct {
|
||||
Artwork string `json:"artwork_url"`
|
||||
Comments int `json:"comment_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
//Duration int `json:"duration"` // there are duration and full_duration fields wtf does that mean
|
||||
Artwork string `json:"artwork_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
Genre string `json:"genre"`
|
||||
Kind string `json:"kind"` // should always be "track"!
|
||||
LastModified string `json:"last_modified"`
|
||||
License string `json:"license"`
|
||||
Likes int64 `json:"likes_count"`
|
||||
Permalink string `json:"permalink"`
|
||||
Played int64 `json:"playback_count"`
|
||||
Reposted int64 `json:"reposts_count"`
|
||||
TagList string `json:"tag_list"`
|
||||
Title string `json:"title"`
|
||||
ID json.Number `json:"id"`
|
||||
Media Media `json:"media"`
|
||||
Authorization string `json:"track_authorization"`
|
||||
Author User `json:"user"`
|
||||
Policy TrackPolicy `json:"policy"`
|
||||
Station string `json:"station_permalink"`
|
||||
Media Media `json:"media"`
|
||||
Author User `json:"user"`
|
||||
Comments int `json:"comment_count"`
|
||||
Likes int64 `json:"likes_count"`
|
||||
Played int64 `json:"playback_count"`
|
||||
Reposted int64 `json:"reposts_count"`
|
||||
Duration uint32 `json:"full_duration"`
|
||||
}
|
||||
|
||||
type TrackPolicy string
|
||||
|
||||
const (
|
||||
PolicyBlock TrackPolicy = "BLOCK" // not available (in your country)
|
||||
PolicySnip TrackPolicy = "SNIP" // 30-second snippet available
|
||||
PolicyAllow TrackPolicy = "ALLOW" // all good
|
||||
PolicyMonetize TrackPolicy = "MONETIZE" // seems like only certain countries get this policy? sometimes protected by widevine and fairplay
|
||||
PolicyBlock TrackPolicy = "BLOCK" // not available (in your country)
|
||||
PolicySnip TrackPolicy = "SNIP" // 30-second snippet available
|
||||
PolicyAllow TrackPolicy = "ALLOW" // all good
|
||||
)
|
||||
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
ProtocolHLS Protocol = "hls"
|
||||
ProtocolProgressive Protocol = "progressive"
|
||||
ProtocolHLS Protocol = "hls"
|
||||
ProtocolProgressive Protocol = "progressive"
|
||||
ProtocolEncryptedHLS Protocol = "encrypted-hls" // idk, haven't seen in the wild
|
||||
ProtocolCTREncryptedHLS Protocol = "ctr-encrypted-hls" // google's widevine
|
||||
ProtocolCBCEncryptedHLS Protocol = "cbc-encrypted-hls" // apple's fairplay
|
||||
)
|
||||
|
||||
type Format struct {
|
||||
@@ -85,8 +89,8 @@ type Stream struct {
|
||||
type Comment struct {
|
||||
Kind string `json:"kind"` // "comment"
|
||||
Body string `json:"body"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Author User `json:"user"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
}
|
||||
|
||||
func (m Media) SelectCompatible(mode string, opus bool) (*Transcoding, string) {
|
||||
@@ -129,7 +133,7 @@ func (m Media) SelectCompatible(mode string, opus bool) (*Transcoding, string) {
|
||||
|
||||
func GetTrack(cid string, permalink string) (Track, error) {
|
||||
tracksCacheLock.RLock()
|
||||
if cell, ok := TracksCache[permalink]; ok && cell.Expires.After(time.Now()) {
|
||||
if cell, ok := TracksCache[permalink]; ok {
|
||||
tracksCacheLock.RUnlock()
|
||||
return cell.Value, nil
|
||||
}
|
||||
@@ -394,7 +398,7 @@ func (t Track) FormatDescription() string {
|
||||
func GetTrackByID(cid string, id string) (Track, error) {
|
||||
tracksCacheLock.RLock()
|
||||
for _, cell := range TracksCache {
|
||||
if string(cell.Value.ID) == string(id) && cell.Expires.After(time.Now()) {
|
||||
if string(cell.Value.ID) == string(id) {
|
||||
tracksCacheLock.RUnlock()
|
||||
return cell.Value, nil
|
||||
}
|
||||
@@ -448,30 +452,6 @@ func GetTrackByID(cid string, id string) (Track, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t Track) DownloadImage() ([]byte, string, error) {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI(t.Artwork)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
//req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") images not big enough to be compressed
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err := DoWithRetry(misc.ImageClient, req, resp)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
return data, string(resp.Header.Peek("Content-Type")), nil
|
||||
}
|
||||
|
||||
func (t Track) Href() string {
|
||||
return "/" + t.Author.Permalink + "/" + t.Permalink
|
||||
}
|
||||
|
||||
@@ -23,21 +23,20 @@ type User struct {
|
||||
Avatar string `json:"avatar_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
Followers int64 `json:"followers_count"`
|
||||
Following int64 `json:"followings_count"`
|
||||
FullName string `json:"full_name"`
|
||||
Kind string `json:"kind"` // should always be "user"!
|
||||
LastModified string `json:"last_modified"`
|
||||
Liked int64 `json:"likes_count"`
|
||||
Permalink string `json:"permalink"`
|
||||
Playlists int64 `json:"playlist_count"`
|
||||
Tracks int64 `json:"track_count"`
|
||||
ID json.Number `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Verified bool `json:"verified"`
|
||||
Station string `json:"station_permalink"`
|
||||
|
||||
WebProfiles []Link
|
||||
WebProfiles []Link `json:",omitempty"`
|
||||
Followers int64 `json:"followers_count"`
|
||||
Following int64 `json:"followings_count"`
|
||||
Liked int64 `json:"likes_count"`
|
||||
Playlists int64 `json:"playlist_count"`
|
||||
Tracks int64 `json:"track_count"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
@@ -54,10 +53,9 @@ const (
|
||||
|
||||
// not worthy of its own file
|
||||
type Repost struct {
|
||||
Type RepostType
|
||||
|
||||
Track *Track // type == track-report
|
||||
Playlist *Playlist // type == playlist-repost
|
||||
Type RepostType
|
||||
}
|
||||
|
||||
func (r Repost) Fix(prefs cfg.Preferences) {
|
||||
@@ -94,7 +92,7 @@ func (l Like) Fix(prefs cfg.Preferences) {
|
||||
}
|
||||
func GetUser(cid string, permalink string) (User, error) {
|
||||
usersCacheLock.RLock()
|
||||
if cell, ok := UsersCache[permalink]; ok && cell.Expires.After(time.Now()) {
|
||||
if cell, ok := UsersCache[permalink]; ok {
|
||||
usersCacheLock.RUnlock()
|
||||
return cell.Value, nil
|
||||
}
|
||||
|
||||
453
main.go
453
main.go
@@ -1,15 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/api"
|
||||
"git.maid.zone/stuff/soundcloak/lib/misc"
|
||||
"github.com/a-h/templ"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -23,8 +28,9 @@ import (
|
||||
proxystreams "git.maid.zone/stuff/soundcloak/lib/proxy_streams"
|
||||
"git.maid.zone/stuff/soundcloak/lib/restream"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
static_files "git.maid.zone/stuff/soundcloak/static"
|
||||
"git.maid.zone/stuff/soundcloak/templates"
|
||||
|
||||
static_files "git.maid.zone/stuff/soundcloak/static"
|
||||
)
|
||||
|
||||
func boolean(b bool) string {
|
||||
@@ -34,85 +40,240 @@ func boolean(b bool) string {
|
||||
return "Disabled"
|
||||
}
|
||||
|
||||
type osfs struct{}
|
||||
type compressionMap = map[string][][]byte
|
||||
|
||||
func (osfs) Open(name string) (fs.File, error) {
|
||||
misc.Log("osfs:", name)
|
||||
func parseCompressionMap(path string, filesystem fs.FS) compressionMap {
|
||||
f, err := filesystem.Open(path + "/.compression")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if strings.HasPrefix(name, "js/") {
|
||||
return os.Open("static/external/" + name[3:])
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open("static/instance/" + name)
|
||||
if err == nil {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
f, err = os.Open("static/assets/" + name)
|
||||
return f, err
|
||||
}
|
||||
|
||||
type staticfs struct {
|
||||
}
|
||||
|
||||
func (staticfs) Open(name string) (fs.File, error) {
|
||||
misc.Log("staticfs:", name)
|
||||
|
||||
if strings.HasPrefix(name, "js/") {
|
||||
return static_files.External.Open("external/" + name[3:])
|
||||
}
|
||||
|
||||
f, err := static_files.Instance.Open("instance/" + name)
|
||||
if err == nil {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
f, err = static_files.Assets.Open("assets/" + name)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// stubby implementation of static middleware
|
||||
// why? mainly because of the pathrewrite
|
||||
// i hate seeing it trying to get root directory
|
||||
func ServeFromFS(r *fiber.App, filesystem fs.FS) {
|
||||
const path = "/_/static"
|
||||
const l = len(path)
|
||||
fs := fasthttp.FS{
|
||||
FS: filesystem,
|
||||
PathRewrite: func(ctx *fasthttp.RequestCtx) []byte {
|
||||
return ctx.Path()[l:]
|
||||
},
|
||||
Compress: true,
|
||||
CompressBrotli: true,
|
||||
CompressedFileSuffixes: map[string]string{
|
||||
"gzip": ".gzip",
|
||||
"br": ".br",
|
||||
"zstd": ".zstd",
|
||||
},
|
||||
}
|
||||
|
||||
handler := fs.NewRequestHandler()
|
||||
|
||||
r.Use(path, func(c fiber.Ctx) error {
|
||||
handler(c.RequestCtx())
|
||||
if c.RequestCtx().Response.StatusCode() == 200 {
|
||||
c.Set("Cache-Control", "public, max-age=28800")
|
||||
sp := bytes.Split(data, []byte("\n"))
|
||||
cm := make(compressionMap, len(sp))
|
||||
for _, h := range sp {
|
||||
sp2 := bytes.Split(h, []byte("|"))
|
||||
if len(sp2) != 2 || string(sp2[0]) == ".gitkeep" {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
h := bytes.Split(sp2[1], []byte(","))
|
||||
if len(h) == 0 || len(h[0]) == 0 {
|
||||
h = nil
|
||||
}
|
||||
|
||||
cm[cfg.B2s(sp2[0])] = h
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
func parseCompressionMaps(filesystem fs.FS) compressionMap {
|
||||
var (
|
||||
external = parseCompressionMap("external", filesystem)
|
||||
assets = parseCompressionMap("assets", filesystem)
|
||||
instance = parseCompressionMap("instance", filesystem)
|
||||
)
|
||||
|
||||
res := make(compressionMap, len(external)+len(assets)+len(instance))
|
||||
|
||||
for k, v := range external {
|
||||
res["external/"+k] = v
|
||||
}
|
||||
for k, v := range assets {
|
||||
res["assets/"+k] = v
|
||||
}
|
||||
for k, v := range instance {
|
||||
res["instance/"+k] = v
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type pooledReader struct {
|
||||
handle io.ReadSeeker
|
||||
p *sync.Pool
|
||||
}
|
||||
|
||||
func (pr *pooledReader) Read(data []byte) (int, error) {
|
||||
return pr.handle.Read(data)
|
||||
}
|
||||
|
||||
func (pr *pooledReader) Close() error {
|
||||
pr.handle.Seek(0, io.SeekStart)
|
||||
pr.p.Put(pr)
|
||||
return nil
|
||||
}
|
||||
|
||||
type pooledFs struct {
|
||||
filesystem fs.FS
|
||||
pools map[string]*sync.Pool
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *pooledFs) Open(name string) (*pooledReader, error) {
|
||||
p.mut.RLock()
|
||||
pool := p.pools[name]
|
||||
p.mut.RUnlock()
|
||||
|
||||
if pool == nil {
|
||||
misc.Log("pool is nil for", name)
|
||||
pool = &sync.Pool{}
|
||||
p.mut.Lock()
|
||||
p.pools[name] = pool
|
||||
p.mut.Unlock()
|
||||
|
||||
goto new
|
||||
}
|
||||
|
||||
if pr := pool.Get(); pr != nil {
|
||||
return pr.(*pooledReader), nil
|
||||
}
|
||||
|
||||
new:
|
||||
h, err := p.filesystem.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pooledReader{h.(io.ReadSeeker), pool}, nil
|
||||
}
|
||||
|
||||
func ServeFS(r *fiber.App, filesystem fs.FS) {
|
||||
cm := parseCompressionMaps(filesystem)
|
||||
misc.Log(cm)
|
||||
|
||||
pfs := &pooledFs{filesystem, make(map[string]*sync.Pool), sync.RWMutex{}}
|
||||
|
||||
const path = "/_/static/"
|
||||
|
||||
if len(cm) == 0 {
|
||||
r.Use(path, func(c fiber.Ctx) error {
|
||||
// start := time.Now()
|
||||
// defer func() { fmt.Println("it took", time.Since(start)) }()
|
||||
fp := cfg.B2s(c.RequestCtx().Path()[len(path):])
|
||||
|
||||
if strings.HasSuffix(fp, ".css") {
|
||||
c.Response().Header.SetContentType("text/css")
|
||||
} else if strings.HasSuffix(fp, ".js") {
|
||||
c.Response().Header.SetContentType("text/javascript")
|
||||
} else if strings.HasSuffix(fp, ".jpg") {
|
||||
c.Response().Header.SetContentType("image/jpeg")
|
||||
} else if strings.HasSuffix(fp, ".ttf") {
|
||||
c.Response().Header.SetContentType("font/ttf")
|
||||
}
|
||||
|
||||
var (
|
||||
f *pooledReader
|
||||
err error
|
||||
)
|
||||
if !strings.HasPrefix(fp, "external/") {
|
||||
f, err = pfs.Open("instance/" + fp)
|
||||
if err != nil {
|
||||
f, err = pfs.Open("assets/" + fp)
|
||||
}
|
||||
} else {
|
||||
f, err = pfs.Open(fp)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Cache-Control", "public, max-age=28800")
|
||||
return c.SendStream(f)
|
||||
})
|
||||
} else {
|
||||
r.Use(path, func(c fiber.Ctx) error {
|
||||
// start := time.Now()
|
||||
// defer func() { fmt.Println("it took", time.Since(start)) }()
|
||||
fp := cfg.B2s(c.RequestCtx().Path()[len(path):])
|
||||
|
||||
var (
|
||||
encs [][]byte
|
||||
ok bool
|
||||
)
|
||||
if strings.HasPrefix(fp, "external/") {
|
||||
encs, ok = cm[fp]
|
||||
} else {
|
||||
encs, ok = cm["instance/"+fp]
|
||||
if ok {
|
||||
fp = "instance/" + fp
|
||||
} else {
|
||||
fp = "assets/" + fp
|
||||
encs, ok = cm[fp]
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
if strings.HasSuffix(fp, ".css") {
|
||||
c.Response().Header.SetContentType("text/css")
|
||||
} else if strings.HasSuffix(fp, ".js") {
|
||||
c.Response().Header.SetContentType("text/javascript")
|
||||
} else if strings.HasSuffix(fp, ".jpg") {
|
||||
c.Response().Header.SetContentType("image/jpeg")
|
||||
} else if strings.HasSuffix(fp, ".ttf") {
|
||||
c.Response().Header.SetContentType("font/ttf")
|
||||
}
|
||||
|
||||
if len(encs) != 0 {
|
||||
ae := c.Request().Header.Peek("Accept-Encoding")
|
||||
if len(ae) == 1 && ae[0] == '*' {
|
||||
c.Response().Header.SetContentEncodingBytes(encs[0])
|
||||
fp += "." + cfg.B2s(encs[0])
|
||||
} else {
|
||||
for _, enc := range encs {
|
||||
if bytes.Contains(ae, enc) {
|
||||
c.Response().Header.SetContentEncodingBytes(enc)
|
||||
fp += "." + cfg.B2s(enc)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f, err := pfs.Open(fp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Cache-Control", "public, max-age=28800")
|
||||
return c.SendStream(f)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func render(c fiber.Ctx, t templ.Component) error {
|
||||
c.Response().Header.SetContentType("text/html")
|
||||
return t.Render(c.RequestCtx(), c.Response().BodyWriter())
|
||||
}
|
||||
|
||||
func r(c fiber.Ctx, title string, content, head templ.Component) error {
|
||||
return render(c, templates.Base(title, content, head))
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := fiber.New(fiber.Config{
|
||||
//Prefork: cfg.Prefork, // moved to ListenConfig in v3
|
||||
JSONEncoder: json.Marshal,
|
||||
JSONDecoder: json.Unmarshal,
|
||||
|
||||
TrustProxy: cfg.TrustedProxyCheck,
|
||||
TrustProxyConfig: fiber.TrustProxyConfig{Proxies: cfg.TrustedProxies},
|
||||
ReadBufferSize: 4096 * 2,
|
||||
})
|
||||
|
||||
if cfg.Debug {
|
||||
app.Server().Logger = fasthttp.Logger(log.New(os.Stdout, "", log.LstdFlags))
|
||||
}
|
||||
|
||||
if !cfg.Debug { // you wanna catch any possible panics as soon as possible
|
||||
app.Use(recover.New())
|
||||
}
|
||||
@@ -150,28 +311,30 @@ func main() {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("", templates.MainPage(prefs), templates.MainPageHead()).Render(c.RequestCtx(), c)
|
||||
return r(c, "", templates.MainPage(prefs), templates.MainPageHead())
|
||||
}
|
||||
|
||||
app.Get("/", mainPageHandler)
|
||||
app.Get("/index.html", mainPageHandler)
|
||||
}
|
||||
|
||||
const AssetsCacheControl = "public, max-age=28800" // 8hrs
|
||||
if cfg.EmbedFiles {
|
||||
misc.Log("using embedded files")
|
||||
ServeFromFS(app, staticfs{})
|
||||
ServeFS(app, static_files.All)
|
||||
} else {
|
||||
misc.Log("loading files dynamically")
|
||||
ServeFromFS(app, osfs{})
|
||||
r, err := os.OpenRoot("static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ServeFS(app, r.FS())
|
||||
}
|
||||
|
||||
// why? because when you load a page without link rel="icon" the browser will
|
||||
// try to load favicon from default location,
|
||||
// and this path loads the user "favicon" by default
|
||||
app.Get("favicon.ico", func(c fiber.Ctx) error {
|
||||
return c.Redirect().To("/_/static/favicon.ico")
|
||||
return c.Redirect().Status(fiber.StatusPermanentRedirect).To("/_/static/favicon.ico")
|
||||
})
|
||||
|
||||
app.Get("robots.txt", func(c fiber.Ctx) error {
|
||||
@@ -185,38 +348,40 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
q := c.Query("q")
|
||||
t := c.Query("type")
|
||||
q := cfg.B2s(c.RequestCtx().QueryArgs().Peek("q"))
|
||||
t := cfg.B2s(c.RequestCtx().QueryArgs().Peek("type"))
|
||||
args := cfg.B2s(c.RequestCtx().QueryArgs().Peek("pagination"))
|
||||
if args == "" {
|
||||
args = "?q=" + url.QueryEscape(q)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "tracks":
|
||||
p, err := sc.SearchTracks("", prefs, c.Query("pagination", "?q="+url.QueryEscape(q)))
|
||||
p, err := sc.SearchTracks("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting tracks for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("tracks: "+q, templates.SearchTracks(p), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "tracks: "+q, templates.SearchTracks(p), nil)
|
||||
|
||||
case "users":
|
||||
p, err := sc.SearchUsers("", prefs, c.Query("pagination", "?q="+url.QueryEscape(q)))
|
||||
p, err := sc.SearchUsers("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting users for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("users: "+q, templates.SearchUsers(p), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "users: "+q, templates.SearchUsers(p), nil)
|
||||
|
||||
case "playlists":
|
||||
p, err := sc.SearchPlaylists("", prefs, c.Query("pagination", "?q="+url.QueryEscape(q)))
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting users for %s: %s\n", q, err)
|
||||
log.Printf("error getting playlists for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("playlists: "+q, templates.SearchPlaylists(p), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "playlists: "+q, templates.SearchPlaylists(p), nil)
|
||||
}
|
||||
|
||||
return c.SendStatus(404)
|
||||
@@ -258,8 +423,6 @@ Disallow: /`)
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
//fmt.Println(c.Hostname(), c.Protocol(), c.IPs())
|
||||
|
||||
u, err := url.Parse(cfg.B2s(loc))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -310,8 +473,7 @@ Disallow: /`)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.TrackEmbed(prefs, track, stream, displayErr).Render(c.RequestCtx(), c)
|
||||
return render(c, templates.TrackEmbed(prefs, track, stream, displayErr))
|
||||
})
|
||||
|
||||
app.Get("/tags/:tag", func(c fiber.Ctx) error {
|
||||
@@ -320,20 +482,14 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
cid, err := sc.GetClientID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag := c.Params("tag")
|
||||
p, err := sc.RecentTracks(cid, prefs, c.Query("pagination", tag+"?limit=20"))
|
||||
p, err := sc.RecentTracks("", prefs, c.Query("pagination", tag+"?limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tagged recent-tracks: %s\n", tag, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("Recent tracks tagged "+tag, templates.RecentTracks(tag, p), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "Recent tracks tagged "+tag, templates.RecentTracks(tag, p), nil)
|
||||
})
|
||||
|
||||
app.Get("/tags/:tag/popular-tracks", func(c fiber.Ctx) error {
|
||||
@@ -342,20 +498,14 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
cid, err := sc.GetClientID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag := c.Params("tag")
|
||||
p, err := sc.SearchTracks(cid, prefs, c.Query("pagination", "?q=*&filter.genre_or_tag="+tag+"&sort=popular"))
|
||||
p, err := sc.SearchTracks("", prefs, c.Query("pagination", "?q=*&filter.genre_or_tag="+tag+"&sort=popular"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tagged popular-tracks: %s\n", tag, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("Popular tracks tagged "+tag, templates.PopularTracks(tag, p), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "Popular tracks tagged "+tag, templates.PopularTracks(tag, p), nil)
|
||||
})
|
||||
|
||||
app.Get("/tags/:tag/playlists", func(c fiber.Ctx) error {
|
||||
@@ -364,37 +514,15 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
cid, err := sc.GetClientID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag := c.Params("tag")
|
||||
// Using a different method, since /playlists/discovery endpoint seems to be broken :P
|
||||
p, err := sc.SearchPlaylists(cid, prefs, c.Query("pagination", "?q=*&filter.genre_or_tag="+tag))
|
||||
p, err := sc.SearchPlaylists("", prefs, c.Query("pagination", "?q=*&filter.genre_or_tag="+tag))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tagged playlists: %s\n", tag, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("Playlists tagged "+tag, templates.TaggedPlaylists(tag, p), nil).Render(c.RequestCtx(), c)
|
||||
})
|
||||
|
||||
app.Get("/_/featured", func(c fiber.Ctx) error {
|
||||
prefs, err := preferences.Get(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracks, err := sc.GetFeaturedTracks("", prefs, c.Query("pagination", "?limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting featured tracks: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("Featured Tracks", templates.FeaturedTracks(tracks), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "Playlists tagged "+tag, templates.TaggedPlaylists(tag, p), nil)
|
||||
})
|
||||
|
||||
app.Get("/discover", func(c fiber.Ctx) error {
|
||||
@@ -409,8 +537,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base("Discover", templates.Discover(selections), nil).Render(c.RequestCtx(), c)
|
||||
return r(c, "Discover", templates.Discover(selections), nil)
|
||||
})
|
||||
|
||||
if cfg.ProxyImages {
|
||||
@@ -421,15 +548,20 @@ Disallow: /`)
|
||||
proxystreams.Load(app)
|
||||
}
|
||||
|
||||
if cfg.EnableAPI {
|
||||
api.Load(app)
|
||||
}
|
||||
|
||||
if cfg.InstanceInfo {
|
||||
type info struct {
|
||||
DefaultPreferences cfg.Preferences
|
||||
Commit string
|
||||
Repo string
|
||||
ProxyImages bool
|
||||
ProxyStreams bool
|
||||
Restream bool
|
||||
GetWebProfiles bool
|
||||
DefaultPreferences cfg.Preferences
|
||||
EnableAPI bool
|
||||
}
|
||||
|
||||
inf, err := json.Marshal(info{
|
||||
@@ -440,13 +572,14 @@ Disallow: /`)
|
||||
Restream: cfg.Restream,
|
||||
GetWebProfiles: cfg.GetWebProfiles,
|
||||
DefaultPreferences: cfg.DefaultPreferences,
|
||||
EnableAPI: cfg.EnableAPI,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("failed to marshal info: ", err)
|
||||
}
|
||||
|
||||
app.Get("/_/info", func(c fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/json")
|
||||
c.Response().Header.SetContentType("application/json")
|
||||
return c.Send(inf)
|
||||
})
|
||||
}
|
||||
@@ -500,8 +633,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserPlaylists(prefs, user, pl), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserPlaylists(prefs, user, pl), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/albums", func(c fiber.Ctx) error {
|
||||
@@ -528,8 +660,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserAlbums(prefs, user, pl), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserAlbums(prefs, user, pl), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/reposts", func(c fiber.Ctx) error {
|
||||
@@ -556,8 +687,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserReposts(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserReposts(prefs, user, p), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/likes", func(c fiber.Ctx) error {
|
||||
@@ -584,8 +714,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserLikes(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserLikes(prefs, user, p), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/popular-tracks", func(c fiber.Ctx) error {
|
||||
@@ -612,8 +741,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserTopTracks(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserTopTracks(prefs, user, p), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/followers", func(c fiber.Ctx) error {
|
||||
@@ -640,8 +768,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserFollowers(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserFollowers(prefs, user, p), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/following", func(c fiber.Ctx) error {
|
||||
@@ -668,8 +795,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserFollowing(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserFollowing(prefs, user, p), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
app.Get("/:user/:track", func(c fiber.Ctx) error {
|
||||
@@ -762,6 +888,17 @@ Disallow: /`)
|
||||
}
|
||||
}
|
||||
|
||||
if *prefs.AutoplayNextRelatedTrack && nextTrack == nil && string(c.RequestCtx().QueryArgs().Peek("playRelated")) != "false" {
|
||||
rel, err := track.GetRelated(cid, prefs, "?limit=4")
|
||||
if err == nil && len(rel.Collection) != 0 {
|
||||
prev := c.RequestCtx().QueryArgs().Peek("prev")
|
||||
nextTrack = &track
|
||||
for i := len(rel.Collection) - 1; i >= 0 && (string(nextTrack.ID) == string(track.ID) || string(nextTrack.ID) == string(prev)); i-- {
|
||||
nextTrack = rel.Collection[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var comments *sc.Paginated[*sc.Comment]
|
||||
if q := c.Query("pagination"); q != "" {
|
||||
comments, err = track.GetComments(cid, prefs, q)
|
||||
@@ -777,8 +914,7 @@ Disallow: /`)
|
||||
downloadAudio = &audio
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, string(c.RequestCtx().QueryArgs().Peek("autoplay")) == "true", playlist, nextTrack, c.Query("volume"), mode, audio, downloadAudio, comments), templates.TrackHeader(prefs, track, true)).Render(c.RequestCtx(), c)
|
||||
return r(c, track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, string(c.RequestCtx().QueryArgs().Peek("autoplay")) == "true", playlist, nextTrack, c.Query("volume"), mode, audio, downloadAudio, comments), templates.TrackHeader(prefs, track, true))
|
||||
})
|
||||
|
||||
app.Get("/_/partials/comments/:id", func(c fiber.Ctx) error {
|
||||
@@ -809,7 +945,7 @@ Disallow: /`)
|
||||
c.Set("next", "done")
|
||||
}
|
||||
|
||||
return templates.Comments(comm).Render(c.RequestCtx(), c)
|
||||
return render(c, templates.Comments(comm))
|
||||
})
|
||||
|
||||
app.Get("/:user", func(c fiber.Ctx) error {
|
||||
@@ -836,8 +972,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(usr.Username, templates.User(prefs, usr, p), templates.UserHeader(usr)).Render(c.RequestCtx(), c)
|
||||
return r(c, usr.Username, templates.User(prefs, usr, p), templates.UserHeader(usr))
|
||||
})
|
||||
|
||||
app.Get("/:user/sets/:playlist", func(c fiber.Ctx) error {
|
||||
@@ -876,8 +1011,7 @@ Disallow: /`)
|
||||
playlist.MissingTracks = strings.Join(next, ",")
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(playlist.Title+" by "+playlist.Author.Username, templates.Playlist(prefs, playlist), templates.PlaylistHeader(playlist)).Render(c.RequestCtx(), c)
|
||||
return r(c, playlist.Title+" by "+playlist.Author.Username, templates.Playlist(prefs, playlist), templates.PlaylistHeader(playlist))
|
||||
})
|
||||
|
||||
app.Get("/:user/_/related", func(c fiber.Ctx) error {
|
||||
@@ -898,14 +1032,13 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
r, err := user.GetRelated(cid, prefs)
|
||||
rel, err := user.GetRelated(cid, prefs)
|
||||
if err != nil {
|
||||
log.Printf("error getting %s related users: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserRelated(prefs, user, r), templates.UserHeader(user)).Render(c.RequestCtx(), c)
|
||||
return r(c, user.Username, templates.UserRelated(prefs, user, rel), templates.UserHeader(user))
|
||||
})
|
||||
|
||||
// I'd like to make this "related" but keeping it "recommended" to have the same url as soundcloud
|
||||
@@ -927,14 +1060,13 @@ Disallow: /`)
|
||||
}
|
||||
track.Postfix(prefs, true)
|
||||
|
||||
r, err := track.GetRelated(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
rel, err := track.GetRelated(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s from %s related tracks: %s\n", c.Params("track"), c.Params("user"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(track.Title+" by "+track.Author.Username, templates.RelatedTracks(track, r), templates.TrackHeader(prefs, track, false)).Render(c.RequestCtx(), c)
|
||||
return r(c, track.Title+" by "+track.Author.Username, templates.RelatedTracks(track, rel), templates.TrackHeader(prefs, track, false))
|
||||
})
|
||||
|
||||
app.Get("/:user/:track/sets", func(c fiber.Ctx) error {
|
||||
@@ -961,8 +1093,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(track.Title+" by "+track.Author.Username, templates.TrackInPlaylists(track, p), templates.TrackHeader(prefs, track, false)).Render(c.RequestCtx(), c)
|
||||
return r(c, track.Title+" by "+track.Author.Username, templates.TrackInPlaylists(track, p), templates.TrackHeader(prefs, track, false))
|
||||
})
|
||||
|
||||
app.Get("/:user/:track/albums", func(c fiber.Ctx) error {
|
||||
@@ -989,8 +1120,7 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(track.Title+" by "+track.Author.Username, templates.TrackInAlbums(track, p), templates.TrackHeader(prefs, track, false)).Render(c.RequestCtx(), c)
|
||||
return r(c, track.Title+" by "+track.Author.Username, templates.TrackInAlbums(track, p), templates.TrackHeader(prefs, track, false))
|
||||
})
|
||||
|
||||
// cute
|
||||
@@ -1036,5 +1166,6 @@ Disallow: /`)
|
||||
if cfg.CodegenConfig {
|
||||
log.Println("Warning: you have CodegenConfig enabled, but the config was loaded dynamically.")
|
||||
}
|
||||
log.Fatal(app.Listen(cfg.Addr, fiber.ListenConfig{EnablePrefork: cfg.Prefork, DisableStartupMessage: true, ListenerNetwork: cfg.Network}))
|
||||
|
||||
log.Fatal(app.Listen(cfg.Addr, fiber.ListenConfig{EnablePrefork: cfg.Prefork, DisableStartupMessage: true, ListenerNetwork: cfg.Network, UnixSocketFileMode: cfg.UnixSocketPerms}))
|
||||
}
|
||||
|
||||
@@ -46,10 +46,8 @@ function getSuggestions() {
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(getSuggestions, 250);
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(getSuggestions, 250);
|
||||
}
|
||||
timeout = setTimeout(getSuggestions, 250);
|
||||
});
|
||||
9
static/assets/keepfocus.js
Normal file
9
static/assets/keepfocus.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var audio = document.getElementById('track');
|
||||
audio.onblur = function (e) {
|
||||
if (e.target != e.relatedTarget) {
|
||||
setTimeout(function() {
|
||||
e.target.focus({preventScroll: true, focusVisible: false});
|
||||
})
|
||||
}
|
||||
}
|
||||
audio.focus({focusVisible: false});
|
||||
@@ -4,11 +4,5 @@ package static
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets/*
|
||||
var Assets embed.FS
|
||||
|
||||
//go:embed instance/*
|
||||
var Instance embed.FS
|
||||
|
||||
//go:embed external/*
|
||||
var External embed.FS
|
||||
//go:embed */*
|
||||
var All embed.FS
|
||||
|
||||
@@ -54,15 +54,12 @@ templ MainPage(p cfg.Preferences) {
|
||||
</form>
|
||||
<footer>
|
||||
<div>
|
||||
<a class="btn" href="/_/featured">Featured Tracks</a>
|
||||
<a class="btn" href="/discover">Discover Playlists</a>
|
||||
<a class="btn" href="/_/preferences">Preferences</a>
|
||||
<a class="btn" href="https://git.maid.zone/stuff/soundcloak">Source code</a>
|
||||
<a class="btn" href="/_/static/notice.txt">Legal notice</a>
|
||||
</div>
|
||||
|
||||
if cfg.Commit != "unknown" {
|
||||
<p style="text-align: center;">Build <a class="link" href={cfg.CommitURL}>{cfg.Commit}</a></p>
|
||||
}
|
||||
<p style="text-align: center;">Build <a class="link" href={cfg.CommitURL}>{cfg.Commit}</a></p>
|
||||
</footer>
|
||||
}
|
||||
|
||||
@@ -2,22 +2,34 @@ package templates
|
||||
|
||||
import (
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
templ FeaturedTracks(p *sc.Paginated[*sc.Track]) {
|
||||
<h1>Featured Tracks</h1>
|
||||
if len(p.Collection) == 0 {
|
||||
<p>no more tracks</p>
|
||||
} else {
|
||||
for _, track := range p.Collection {
|
||||
@TrackItem(track, true, "")
|
||||
templ PlaylistOrUserItem(pl *sc.PlaylistOrUser) {
|
||||
<a class="listing" href={ templ.SafeURL(pl.Href()) }>
|
||||
{{
|
||||
img := pl.Artwork
|
||||
if pl.Kind == "user" {
|
||||
img = pl.Avatar
|
||||
}
|
||||
}}
|
||||
if img != "" {
|
||||
<img loading="lazy" fetchpriority="low" src={ img }/>
|
||||
} else {
|
||||
<img loading="lazy" fetchpriority="low" src="/_/static/placeholder.jpg"/>
|
||||
}
|
||||
if p.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("/_/featured?pagination=" + url.QueryEscape(p.Next[sc.H+len("/featured_tracks/top/all-music"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
}
|
||||
<div class="meta">
|
||||
if pl.Kind == "user" {
|
||||
<h3>{ pl.Username }</h3>
|
||||
if pl.FullName != "" {
|
||||
<span>{ pl.FullName }</span>
|
||||
}
|
||||
} else {
|
||||
<h3>{ pl.Title }</h3>
|
||||
<p>{ strconv.FormatInt(pl.TrackCount, 10) } tracks</p>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ Discover(p *sc.Paginated[*sc.Selection]) {
|
||||
@@ -32,8 +44,7 @@ templ Discover(p *sc.Paginated[*sc.Selection]) {
|
||||
|
||||
<h2>{selection.Title}</h2>
|
||||
for _, pl := range selection.Items.Collection {
|
||||
// We don't need the username
|
||||
@PlaylistItem(pl, false)
|
||||
@PlaylistOrUserItem(pl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
templ Description(prefs cfg.Preferences, text string, injected templ.Component) {
|
||||
if text != "" || injected != nil {
|
||||
<details>
|
||||
<summary>Toggle description</summary>
|
||||
<summary>Description</summary>
|
||||
<p style="white-space: pre-wrap;">
|
||||
if text != "" {
|
||||
if *prefs.ParseDescriptions {
|
||||
|
||||
@@ -42,50 +42,13 @@ templ sel_audio(name string, selected string, noOpus bool) {
|
||||
templ Preferences(prefs cfg.Preferences) {
|
||||
<h1>Preferences</h1>
|
||||
<form method="post" autocomplete="off">
|
||||
<label>
|
||||
Parse descriptions:
|
||||
@checkbox("ParseDescriptions", *prefs.ParseDescriptions)
|
||||
</label>
|
||||
<label>
|
||||
Show current audio:
|
||||
@checkbox("ShowAudio", *prefs.ShowAudio)
|
||||
</label>
|
||||
if cfg.ProxyImages {
|
||||
<label>
|
||||
Proxy images:
|
||||
@checkbox("ProxyImages", *prefs.ProxyImages)
|
||||
</label>
|
||||
}
|
||||
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; you need to allow autoplay from this domain!!)
|
||||
</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>
|
||||
Fetch search suggestions:
|
||||
@checkbox("SearchSuggestions", *prefs.SearchSuggestions)
|
||||
(requires JS)
|
||||
</label>
|
||||
<label>
|
||||
Dynamically load comments:
|
||||
@checkbox("DynamicLoadComments", *prefs.DynamicLoadComments)
|
||||
(requires JS)
|
||||
</label>
|
||||
<h1>Player preferences</h1>
|
||||
<label>
|
||||
Player:
|
||||
@sel("Player", []option{
|
||||
@@ -96,7 +59,6 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
</label>
|
||||
switch *prefs.Player {
|
||||
case cfg.HLSPlayer:
|
||||
<h1>Player-specific preferences</h1>
|
||||
if cfg.ProxyStreams {
|
||||
<label>
|
||||
Proxy song streams:
|
||||
@@ -112,12 +74,60 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
@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>
|
||||
}
|
||||
<h1>Frontend enhancements</h1>
|
||||
if cfg.ProxyImages {
|
||||
<label>
|
||||
Proxy images:
|
||||
@checkbox("ProxyImages", *prefs.ProxyImages)
|
||||
</label>
|
||||
}
|
||||
<label>
|
||||
Parse descriptions:
|
||||
@checkbox("ParseDescriptions", *prefs.ParseDescriptions)
|
||||
</label>
|
||||
<label>
|
||||
Show current audio:
|
||||
@checkbox("ShowAudio", *prefs.ShowAudio)
|
||||
</label>
|
||||
<label>
|
||||
Fetch search suggestions:
|
||||
@checkbox("SearchSuggestions", *prefs.SearchSuggestions)
|
||||
(requires JS)
|
||||
</label>
|
||||
<label>
|
||||
Dynamically load comments:
|
||||
@checkbox("DynamicLoadComments", *prefs.DynamicLoadComments)
|
||||
(requires JS)
|
||||
</label>
|
||||
<label>
|
||||
Keep player focus:
|
||||
@checkbox("KeepPlayerFocus", *prefs.KeepPlayerFocus)
|
||||
(requires JS)
|
||||
</label>
|
||||
<h2 style="margin-bottom: .35rem">Autoplay</h2>
|
||||
<i>Requires JS. You also need to allow autoplay from this domain</i>
|
||||
<label style="margin-top: 1rem">
|
||||
Autoplay next track in playlists:
|
||||
@checkbox("AutoplayNextTrack", *prefs.AutoplayNextTrack)
|
||||
</label>
|
||||
if *prefs.AutoplayNextTrack {
|
||||
<label>
|
||||
Default autoplay mode (in playlists):
|
||||
@sel("DefaultAutoplayMode", []option{
|
||||
{"normal", "Normal (play songs in order)", false},
|
||||
{"random", "Random (play random song)", false},
|
||||
}, *prefs.DefaultAutoplayMode)
|
||||
</label>
|
||||
}
|
||||
<label>
|
||||
Autoplay next related track:
|
||||
@checkbox("AutoplayNextRelatedTrack", *prefs.AutoplayNextRelatedTrack)
|
||||
</label>
|
||||
<input type="submit" value="Update" class="btn" style="margin-top: 1rem;"/>
|
||||
<p>These preferences get saved in a cookie.</p>
|
||||
</form>
|
||||
@@ -132,6 +142,5 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
<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>
|
||||
}
|
||||
|
||||
@@ -47,22 +47,36 @@ templ TrackHeader(prefs cfg.Preferences, t sc.Track, needPlayer bool) {
|
||||
<meta name="og:description" content={ t.FormatDescription() }/>
|
||||
<meta name="og:image" content={ t.Artwork }/>
|
||||
<link rel="icon" type="image/x-icon" href={ t.Artwork }/>
|
||||
if needPlayer && *prefs.Player == cfg.HLSPlayer {
|
||||
<script src="/_/static/js/hls.light.min.js"></script>
|
||||
if needPlayer {
|
||||
if *prefs.Player == cfg.HLSPlayer {
|
||||
<script src="/_/static/external/hls.light.min.js"></script>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func next(t *sc.Track, p *sc.Playlist, autoplay bool, mode string, volume string) string {
|
||||
r := t.Href() + "?playlist=" + p.Href()[1:]
|
||||
if autoplay {
|
||||
r += "&autoplay=true"
|
||||
}
|
||||
if mode != "" {
|
||||
r += "&mode=" + mode
|
||||
func next(c *sc.Track, t *sc.Track, p *sc.Playlist, mode string, volume string) string {
|
||||
r := t.Href()
|
||||
|
||||
if p != nil {
|
||||
r += "?playlist=" + p.Href()[1:]
|
||||
if mode != "" {
|
||||
r += "&mode=" + mode
|
||||
}
|
||||
r += "&"
|
||||
} else {
|
||||
r += "?"
|
||||
|
||||
if c != nil {
|
||||
r += "prev=" + string(c.ID) + "&"
|
||||
}
|
||||
}
|
||||
|
||||
r += "autoplay=true"
|
||||
|
||||
if volume != "" {
|
||||
r += "&volume=" + volume
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -71,19 +85,22 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
|
||||
{{ return }}
|
||||
}
|
||||
if displayErr == "" {
|
||||
{{ var audioPref string }}
|
||||
{{ var audioPref *string }}
|
||||
if cfg.Restream && *prefs.Player == cfg.RestreamPlayer {
|
||||
{{ audioPref = *prefs.RestreamAudio }}
|
||||
{{ audioPref = prefs.RestreamAudio }}
|
||||
if nextTrack != nil {
|
||||
<audio id="track" src={ "/_/restream" + track.Href() } 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(&track, nextTrack, playlist, mode, "") } volume={ volume }></audio>
|
||||
<script async src="/_/static/restream.js"></script>
|
||||
} else {
|
||||
<audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
|
||||
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
|
||||
}
|
||||
if *prefs.KeepPlayerFocus {
|
||||
<script async src="/_/static/keepfocus.js"></script>
|
||||
}
|
||||
} else if stream != "" {
|
||||
{{ audioPref = *prefs.HLSAudio }}
|
||||
{{ audioPref = prefs.HLSAudio }}
|
||||
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={ stream } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, mode, "") } volume={ volume }></audio>
|
||||
} else {
|
||||
<audio id="track" src={ stream } controls autoplay?={ autoplay }></audio>
|
||||
}
|
||||
@@ -92,6 +109,9 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
|
||||
} else {
|
||||
<script async src="/_/static/player.js"></script>
|
||||
}
|
||||
if *prefs.KeepPlayerFocus {
|
||||
<script async src="/_/static/keepfocus.js"></script>
|
||||
}
|
||||
<noscript>
|
||||
<br/>
|
||||
JavaScript is disabled! Audio playback may not work without it enabled.
|
||||
@@ -108,7 +128,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
|
||||
}
|
||||
if *prefs.ShowAudio {
|
||||
<div>
|
||||
if audioPref == cfg.AudioBest {
|
||||
if *audioPref == cfg.AudioBest {
|
||||
<p>Audio: best ({ audio })</p>
|
||||
} else {
|
||||
<p>Audio: { audio }</p>
|
||||
@@ -151,7 +171,7 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
|
||||
}
|
||||
<h1>{ t.Title }</h1>
|
||||
@TrackPlayer(prefs, t, stream, displayErr, autoplay, nextTrack, playlist, volume, mode, audio)
|
||||
if cfg.Restream {
|
||||
if displayErr == "" && cfg.Restream {
|
||||
<div style="display: flex; margin-bottom: 1rem;">
|
||||
<a class="btn" href={ templ.SafeURL("/_/restream" + t.Href() + "?metadata=true") } download={ t.Permalink + "." + toExt(*downloadAudio) }>download</a>
|
||||
</div>
|
||||
@@ -159,19 +179,25 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
|
||||
if t.Genre != "" {
|
||||
<a href={ templ.SafeURL("/tags/" + t.Genre) }><p class="tag">{ t.Genre }</p></a>
|
||||
}
|
||||
if playlist != nil {
|
||||
if nextTrack != nil {
|
||||
<details open style="margin-bottom: 1rem;">
|
||||
<summary>Playback info</summary>
|
||||
<h2>In playlist:</h2>
|
||||
@PlaylistItem(playlist, true)
|
||||
if playlist != nil {
|
||||
<h2>In playlist:</h2>
|
||||
@PlaylistItem(playlist, true)
|
||||
}
|
||||
<h2>Next track:</h2>
|
||||
@TrackItem(nextTrack, true, next(nextTrack, playlist, true, mode, volume))
|
||||
@TrackItem(nextTrack, true, next(&t, nextTrack, playlist, mode, volume))
|
||||
<div style="display: flex; gap: 1rem">
|
||||
<a href={ templ.SafeURL(t.Href()) } class="btn">Stop playlist playback</a>
|
||||
if mode != cfg.AutoplayRandom {
|
||||
<a href={ templ.SafeURL(next(&t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
|
||||
if playlist != nil {
|
||||
<a href={ templ.SafeURL(t.Href()) } class="btn">Stop playlist playback</a>
|
||||
if mode != cfg.AutoplayRandom {
|
||||
<a href={ templ.SafeURL(next(nil, &t, playlist, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
|
||||
} else {
|
||||
<a href={ templ.SafeURL(next(nil, &t, playlist, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
|
||||
}
|
||||
} else {
|
||||
<a href={ templ.SafeURL(next(&t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
|
||||
<a href={ templ.SafeURL(t.Href() + "?playRelated=false") } class="btn">Stop playback</a>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
@@ -194,6 +220,7 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
|
||||
if t.License != "" {
|
||||
<p>License: { t.License }</p>
|
||||
}
|
||||
<p>Policy: { t.Policy }</p>
|
||||
if t.TagList != "" {
|
||||
<p>Tags: { strings.Join(sc.TagListParser(t.TagList), ", ") }</p>
|
||||
}
|
||||
@@ -251,7 +278,7 @@ templ TrackEmbed(prefs cfg.Preferences, t sc.Track, stream string, displayErr st
|
||||
<link rel="stylesheet" href="/_/static/global.css"/>
|
||||
<title>soundcloak</title>
|
||||
if *prefs.Player == cfg.HLSPlayer && stream != "" {
|
||||
<script src="/_/static/js/hls.light.js"></script>
|
||||
<script src="/_/static/external/hls.light.min.js"></script>
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
templ UserHeader(u sc.User) {
|
||||
@@ -115,7 +114,7 @@ templ User(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Track]) {
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(u.Tracks) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/tracks")[1])) } rel="noreferrer">more tracks</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/tracks"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more tracks</span>
|
||||
@@ -133,7 +132,7 @@ templ UserPlaylists(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playli
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/playlists_without_albums")[1])) } rel="noreferrer">more playlists</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/playlists_without_albums"):])) } rel="noreferrer">more playlists</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more playlists</span>
|
||||
@@ -151,7 +150,7 @@ templ UserAlbums(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playlist]
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/albums")[1])) } rel="noreferrer">more albums</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/albums"):])) } rel="noreferrer">more albums</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more albums</span>
|
||||
@@ -173,7 +172,7 @@ templ UserReposts(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Repost])
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/reposts")[1])) } rel="noreferrer">more reposts</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/stream/users/")+len(u.ID)+len("/reposts"):])) } rel="noreferrer">more reposts</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more reposts</span>
|
||||
@@ -195,7 +194,7 @@ templ UserLikes(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Like]) {
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/likes")[1])) } rel="noreferrer">more likes</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/likes"):])) } rel="noreferrer">more likes</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more likes</span>
|
||||
@@ -243,7 +242,7 @@ templ UserFollowers(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.User])
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/followers")[1])) } rel="noreferrer">more users</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/followers"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more users</span>
|
||||
@@ -261,7 +260,7 @@ templ UserFollowing(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.User])
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/following")[1])) } rel="noreferrer">more users</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/followings"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more users</span>
|
||||
@@ -281,7 +280,7 @@ templ SearchUsers(p *sc.Paginated[*sc.User]) {
|
||||
@UserItem(user)
|
||||
}
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=users&pagination=" + url.QueryEscape(strings.Split(p.Next, "/users")[1])) } rel="noreferrer">more users</a>
|
||||
<a class="btn" href={ templ.SafeURL("?type=users&pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/users"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user