Compare commits
109 Commits
fix_token_
...
59b702aa15
Author | SHA1 | Date | |
---|---|---|---|
59b702aa15 | |||
edf0109095 | |||
ecf0f91464 | |||
a12596e1a2 | |||
df8d36c661 | |||
5018184e7c | |||
5c73f043cd | |||
013e805ece | |||
21da37246e | |||
fb0814babd | |||
bae220d4fe | |||
97def1bab1 | |||
10d5b4a583 | |||
feda45f803 | |||
f8f964741a | |||
58c736c51b | |||
336d5dcaa7 | |||
4c00e4ac05 | |||
92afbe55de | |||
9133062e68 | |||
83d50316fb | |||
e187ccd14a | |||
284e03ccf9 | |||
8a3ceaf94a | |||
bd47c206a1 | |||
7a099f259f | |||
4ea911e6b2 | |||
31d68afdc9 | |||
96a7e155c5 | |||
045a8852ec | |||
4cc8bf8318 | |||
1715b36ae9 | |||
6102b08894 | |||
892b0e89c8 | |||
a64e2143f3 | |||
e13d9b7239 | |||
e597ed8f06 | |||
8c67d33721 | |||
26aa374bbd | |||
5b8b1e36ca | |||
093d240530 | |||
9048565d48 | |||
6b2aab23c8 | |||
6b11d936b3 | |||
34692359cf | |||
33411a7588 | |||
5a0f0f96f5 | |||
273d889f1b | |||
93594fc642 | |||
af6f3d3b3f | |||
62b791bb24 | |||
e9af28b6eb | |||
2f2cded671 | |||
b22fb7cd7b | |||
75b0149313 | |||
50fad938dd | |||
b6f5831d10 | |||
565b50646f | |||
6484ebf897 | |||
3f863c8991 | |||
e581f432dd | |||
2c8f5a7ac1 | |||
6d83b07aaa | |||
5f67eacf93 | |||
6684db5e82 | |||
1760f5a676 | |||
8307aa1e42 | |||
27f25e0fb1 | |||
c15f25e27b | |||
20e3129d88 | |||
54c2ffad95 | |||
6991fc6ae3 | |||
30b27aea44 | |||
63e2fadf4a | |||
4ba26285d7 | |||
89140c8cf7 | |||
99048c4683 | |||
ccfe7d0eeb | |||
f7c182dcd8 | |||
9bd540d659 | |||
4f6a14739b | |||
7c87d63d34 | |||
75b139dff2 | |||
10499df423 | |||
c86ca16c1a | |||
858299c861 | |||
75f5c6668c | |||
e6b9a2e426 | |||
d4a2b3edc6 | |||
4f0b29f930 | |||
4e2648280d | |||
35ae71302f | |||
e79242c9e7 | |||
da581cb79b | |||
3f4526debe | |||
5de171b13a | |||
22910956db | |||
94ada2b10c | |||
1f246c956d | |||
b7778d5f95 | |||
f507fcfcf8 | |||
c6030064f1 | |||
fe6123e05f | |||
3bb5dc5f3e | |||
35927287f1 | |||
469d0994f1 | |||
99097da6b8 | |||
3d2c936a9e | |||
03e267f02e |
48
.env.example
Normal file
@ -0,0 +1,48 @@
|
||||
# Redlib configuration
|
||||
# See the Configuration section of the README for a more detailed explanation of these settings.
|
||||
|
||||
# Instance-specific settings
|
||||
# Enable SFW-only mode for the instance
|
||||
REDLIB_SFW_ONLY=off
|
||||
# Set a banner message for the instance
|
||||
REDLIB_BANNER=
|
||||
# Disable search engine indexing
|
||||
REDLIB_ROBOTS_DISABLE_INDEXING=off
|
||||
# Set the Pushshift frontend for "removed" links
|
||||
REDLIB_PUSHSHIFT_FRONTEND=undelete.pullpush.io
|
||||
|
||||
# Default user settings
|
||||
# Set the default theme (options: system, light, dark, black, dracula, nord, laserwave, violet, gold, rosebox, gruvboxdark, gruvboxlight)
|
||||
REDLIB_DEFAULT_THEME=system
|
||||
# Set the default front page (options: default, popular, all)
|
||||
REDLIB_DEFAULT_FRONT_PAGE=default
|
||||
# Set the default layout (options: card, clean, compact)
|
||||
REDLIB_DEFAULT_LAYOUT=card
|
||||
# Enable wide mode by default
|
||||
REDLIB_DEFAULT_WIDE=off
|
||||
# Set the default post sort method (options: hot, new, top, rising, controversial)
|
||||
REDLIB_DEFAULT_POST_SORT=hot
|
||||
# Set the default comment sort method (options: confidence, top, new, controversial, old)
|
||||
REDLIB_DEFAULT_COMMENT_SORT=confidence
|
||||
# Enable showing NSFW content by default
|
||||
REDLIB_DEFAULT_SHOW_NSFW=off
|
||||
# Enable blurring NSFW content by default
|
||||
REDLIB_DEFAULT_BLUR_NSFW=off
|
||||
# Enable HLS video format by default
|
||||
REDLIB_DEFAULT_USE_HLS=off
|
||||
# Hide HLS notification by default
|
||||
REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
||||
# Disable autoplay videos by default
|
||||
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
|
||||
REDLIB_DEFAULT_SUBSCRIPTIONS=
|
||||
# Hide awards by default
|
||||
REDLIB_DEFAULT_HIDE_AWARDS=off
|
||||
# Hide sidebar and summary
|
||||
REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY=off
|
||||
# Disable the confirmation before visiting Reddit
|
||||
REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
||||
# Hide score by default
|
||||
REDLIB_DEFAULT_HIDE_SCORE=off
|
||||
# Enable fixed navbar by default
|
||||
REDLIB_DEFAULT_FIXED_NAVBAR=on
|
75
.github/workflows/build-artifacts.yaml
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "compose.*"
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc
|
||||
CC_aarch64_unknown_linux_musl: aarch64-linux-gnu-gcc
|
||||
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-gcc
|
||||
CC_armv7_unknown_linux_musleabihf: arm-linux-gnueabihf-gcc
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Rust project - latest
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-unknown-linux-musl
|
||||
- aarch64-unknown-linux-musl
|
||||
- armv7-unknown-linux-musleabihf
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends musl-tools
|
||||
|
||||
- if: matrix.target == 'armv7-unknown-linux-musleabihf'
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y gcc-arm-linux-gnueabihf musl-tools
|
||||
|
||||
- if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y gcc-aarch64-linux-gnu musl-tools
|
||||
|
||||
- name: Versions
|
||||
id: version
|
||||
run: echo "VERSION=$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Package release
|
||||
run: tar czf redlib-${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release/ redlib
|
||||
|
||||
- name: Upload release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.base_ref != 'main' && github.event_name == 'release'
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.VERSION }}
|
||||
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
||||
draft: true
|
||||
files: |
|
||||
redlib-${{ matrix.target }}.tar.gz
|
||||
body: |
|
||||
- ${{ github.event.head_commit.message }} ${{ github.sha }}
|
||||
generate_release_notes: true
|
||||
|
||||
|
||||
|
135
.github/workflows/main-docker.yml
vendored
@ -1,44 +1,112 @@
|
||||
name: Docker Build
|
||||
name: Container build
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_run:
|
||||
workflows: ["Release Build"]
|
||||
types:
|
||||
- completed
|
||||
env:
|
||||
REGISTRY_IMAGE: quay.io/redlib/redlib
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- { platform: 'linux/amd64', tag: 'latest', dockerfile: 'Dockerfile' }
|
||||
- { platform: 'linux/arm64', tag: 'latest-arm', dockerfile: 'Dockerfile.arm' }
|
||||
- { platform: 'linux/arm/v7', tag: 'latest-armv7', dockerfile: 'Dockerfile.armv7' }
|
||||
include:
|
||||
- { platform: linux/amd64, target: x86_64-unknown-linux-musl}
|
||||
- { platform: linux/arm64, target: aarch64-unknown-linux-musl}
|
||||
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Login to Quay.io
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Quay.io Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
file: Dockerfile
|
||||
build-args: TARGET=${{ matrix.target }}
|
||||
-
|
||||
name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
-
|
||||
name: Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
-
|
||||
name: Login to Quay.io Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: push README to Quay.io
|
||||
- name: Push README to Quay.io
|
||||
uses: christian-korneck/update-container-description-action@v1
|
||||
env:
|
||||
DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
|
||||
@ -47,13 +115,8 @@ jobs:
|
||||
provider: quay
|
||||
readme_file: 'README.md'
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./${{ matrix.config.dockerfile }}
|
||||
platforms: ${{ matrix.config.platform }}
|
||||
push: true
|
||||
tags: quay.io/redlib/redlib:${{ matrix.config.tag }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
|
16
.github/workflows/main-rust.yml
vendored
@ -30,9 +30,15 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install musl-gcc
|
||||
run: sudo apt-get install musl-tools
|
||||
|
||||
- name: Install cargo musl target
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
# Building actions
|
||||
- name: Build
|
||||
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu
|
||||
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
- name: Versions
|
||||
id: version
|
||||
@ -45,17 +51,17 @@ jobs:
|
||||
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Calculate SHA512 checksum
|
||||
run: sha512sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha512
|
||||
run: sha512sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha512
|
||||
|
||||
- name: Calculate SHA256 checksum
|
||||
run: sha256sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha256
|
||||
run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: Upload a Build Artifact
|
||||
with:
|
||||
name: redlib
|
||||
path: |
|
||||
target/x86_64-unknown-linux-gnu/release/redlib
|
||||
target/x86_64-unknown-linux-musl/release/redlib
|
||||
redlib.sha512
|
||||
redlib.sha256
|
||||
|
||||
@ -68,7 +74,7 @@ jobs:
|
||||
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
||||
draft: true
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/redlib
|
||||
target/x86_64-unknown-linux-musl/release/redlib
|
||||
redlib.sha512
|
||||
redlib.sha256
|
||||
body: |
|
||||
|
2
.replit
@ -1,2 +1,2 @@
|
||||
run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-gnu/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done"
|
||||
run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-musl/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done"
|
||||
language = "bash"
|
||||
|
664
Cargo.lock
generated
18
Cargo.toml
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "redlib"
|
||||
name = "redsunlib"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/redlib-org/redlib"
|
||||
version = "0.31.0"
|
||||
repository = "https://git.stardust.wtf/iridium/redlib"
|
||||
version = "0.34.0"
|
||||
authors = [
|
||||
"Matthew Esposito <matt+cargo@matthew.science>",
|
||||
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
|
||||
@ -11,8 +11,8 @@ authors = [
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.11.1", default-features = false }
|
||||
cached = { version = "0.46.1", features = ["async"] }
|
||||
askama = { version = "0.12.1", default-features = false }
|
||||
cached = { version = "0.51.3", features = ["async"] }
|
||||
clap = { version = "4.4.11", default-features = false, features = [
|
||||
"std",
|
||||
"env",
|
||||
@ -20,9 +20,9 @@ clap = { version = "4.4.11", default-features = false, features = [
|
||||
regex = "1.10.2"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
cookie = "0.18.0"
|
||||
futures-lite = "1.13.0"
|
||||
futures-lite = "2.2.0"
|
||||
hyper = { version = "0.14.28", features = ["full"] }
|
||||
hyper-rustls = "0.24.2"
|
||||
hyper-rustls = "0.25.0"
|
||||
percent-encoding = "2.3.1"
|
||||
route-recognizer = "0.3.1"
|
||||
serde_json = "1.0.108"
|
||||
@ -31,13 +31,13 @@ time = { version = "0.3.31", features = ["local-offset"] }
|
||||
url = "2.5.0"
|
||||
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
|
||||
libflate = "2.0.0"
|
||||
brotli = { version = "3.4.0", features = ["std"] }
|
||||
brotli = { version = "6.0.0", features = ["std"] }
|
||||
toml = "0.8.8"
|
||||
once_cell = "1.19.0"
|
||||
serde_yaml = "0.9.29"
|
||||
build_html = "2.4.0"
|
||||
uuid = { version = "1.6.1", features = ["v4"] }
|
||||
base64 = "0.21.5"
|
||||
base64 = "0.22.1"
|
||||
fastrand = "2.0.1"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5.0"
|
||||
|
22
Dockerfile
@ -1,19 +1,17 @@
|
||||
####################################################################################################
|
||||
## Builder
|
||||
####################################################################################################
|
||||
|
||||
FROM rust:alpine AS builder
|
||||
|
||||
RUN apk add --no-cache musl-dev
|
||||
RUN apk add --no-cache musl-dev git
|
||||
|
||||
WORKDIR /redlib
|
||||
WORKDIR /redsunlib
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||
|
||||
####################################################################################################
|
||||
## Final image
|
||||
####################################################################################################
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
# Import ca-certificates from builder
|
||||
@ -21,16 +19,16 @@ COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy our build
|
||||
COPY --from=builder /redlib/target/x86_64-unknown-linux-musl/release/redlib /usr/local/bin/redlib
|
||||
COPY --from=builder /redsunlib/target/x86_64-unknown-linux-musl/release/redsunlib /usr/local/bin/redsunlib
|
||||
|
||||
# Use an unprivileged user.
|
||||
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
|
||||
USER redlib
|
||||
RUN adduser --home /nonexistent --no-create-home --disabled-password redsunlib
|
||||
USER redsunlib
|
||||
|
||||
# Tell Docker to expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run a healthcheck every minute to make sure redlib is functional
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||
# Run a healthcheck every minute to make sure redsunlib is functional
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
|
||||
|
||||
CMD ["redlib"]
|
||||
CMD ["redsunlib"]
|
@ -1,45 +0,0 @@
|
||||
####################################################################################################
|
||||
## Builder
|
||||
####################################################################################################
|
||||
FROM rust:alpine AS builder
|
||||
|
||||
RUN apk add --no-cache g++ git
|
||||
|
||||
WORKDIR /usr/src/redlib
|
||||
|
||||
# cache dependencies in their own layer
|
||||
COPY Cargo.lock Cargo.toml .
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo install --config net.git-fetch-with-cli=true --path . && rm -rf ./src
|
||||
|
||||
COPY . .
|
||||
|
||||
# net.git-fetch-with-cli is specified in order to prevent a potential OOM kill
|
||||
# in low memory environments. See:
|
||||
# https://users.rust-lang.org/t/cargo-uses-too-much-memory-being-run-in-qemu/76531
|
||||
# This is tracked under issue #641. This also requires us to install git in the
|
||||
# builder.
|
||||
RUN cargo install --config net.git-fetch-with-cli=true --path .
|
||||
|
||||
####################################################################################################
|
||||
## Final image
|
||||
####################################################################################################
|
||||
FROM alpine:latest
|
||||
|
||||
# Import ca-certificates from builder
|
||||
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy our build
|
||||
COPY --from=builder /usr/local/cargo/bin/redlib /usr/local/bin/redlib
|
||||
|
||||
# Use an unprivileged user.
|
||||
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
|
||||
USER redlib
|
||||
|
||||
# Tell Docker to expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run a healthcheck every minute to make sure redlib is functional
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||
|
||||
CMD ["redlib"]
|
@ -1,43 +0,0 @@
|
||||
####################################################################################################
|
||||
## Builder
|
||||
####################################################################################################
|
||||
FROM --platform=$BUILDPLATFORM rust:slim AS builder
|
||||
|
||||
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
ENV CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc
|
||||
|
||||
RUN apt-get update && apt-get -y install gcc-arm-linux-gnueabihf \
|
||||
binutils-arm-linux-gnueabihf \
|
||||
musl-tools
|
||||
|
||||
RUN rustup target add armv7-unknown-linux-musleabihf
|
||||
|
||||
WORKDIR /redlib
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --target armv7-unknown-linux-musleabihf --release
|
||||
|
||||
####################################################################################################
|
||||
## Final image
|
||||
####################################################################################################
|
||||
FROM alpine:latest
|
||||
|
||||
# Import ca-certificates from builder
|
||||
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy our build
|
||||
COPY --from=builder /redlib/target/armv7-unknown-linux-musleabihf/release/redlib /usr/local/bin/redlib
|
||||
|
||||
# Use an unprivileged user.
|
||||
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
|
||||
USER redlib
|
||||
|
||||
# Tell Docker to expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run a healthcheck every minute to make sure redlib is functional
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||
|
||||
CMD ["redlib"]
|
428
README.md
@ -1,33 +1,60 @@
|
||||
# Redlib
|
||||
<img align="left" width="128" height="128" src="https://git.stardust.wtf/attachments/842086e3-b718-4379-b718-c3a542842152" alt="logo">
|
||||
|
||||
> An alternative private front-end to Reddit, with its origins in [Libreddit](https://github.com/libreddit/libreddit).
|
||||
# Redsunlib
|
||||
> An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some function and cosmetic changes.
|
||||
|
||||

|
||||
<br>
|
||||
|
||||
---
|
||||

|
||||
|
||||
**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://redlib.matthew.science/r/unpopularopinion) without being [tracked](#reddit).
|
||||
## Table of Contents
|
||||
|
||||
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
|
||||
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
|
||||
- 🕵 Private: all requests are proxied through the server, including media
|
||||
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
|
||||
1. [Redlib](#redlib)
|
||||
2. [Instances](#instances)
|
||||
3. [About](#about)
|
||||
- [Built with](#built-with)
|
||||
- [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends)
|
||||
- [Teddit](#teddit)
|
||||
- [Libreddit](#libreddit)
|
||||
4. [Comparison](#comparison)
|
||||
- [Speed](#speed)
|
||||
- [Privacy](#privacy)
|
||||
- [Reddit](#reddit)
|
||||
- [Redlib](#redlib-1)
|
||||
- [Server](#server)
|
||||
5. [Deployment](#deployment)
|
||||
- [Docker](#docker)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [Docker CLI](#docker-cli)
|
||||
- [Binary](#binary)
|
||||
- [Running as a systemd service](#running-as-a-systemd-service)
|
||||
- [Building from source](#building-from-source)
|
||||
- [Replit/Heroku/Glitch](#replit-heroku-glitch)
|
||||
- [launchd (macOS)](#launchd-macos)
|
||||
6. [Configuration](#configuration)
|
||||
- [Instance settings](#instance-settings)
|
||||
- [Default user settings](#default-user-settings)
|
||||
|
||||
---
|
||||
|
||||
# Instances
|
||||
|
||||
🔗 **Want to automatically redirect Reddit links to Redlib? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
|
||||
> [!TIP]
|
||||
> 🔗 **Want to automatically redirect Reddit links to Redlib? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
|
||||
|
||||
[Follow this link](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) for an up-to-date table of instances in Markdown format. This list is also available as [a machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json).
|
||||
An up-to-date table of instances is available in [Markdown](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) and [machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json).
|
||||
|
||||
Both files are part of the [redlib-instances](https://github.com/redlib-org/redlib-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [redlib-instances README](https://github.com/redlib-org/redlib-instances/blob/main/README.md).
|
||||
|
||||
For information on instance uptime, see the [Uptime Robot status page](https://stats.uptimerobot.com/mpmqAs1G2Q).
|
||||
|
||||
---
|
||||
|
||||
# About
|
||||
|
||||
Find Redlib on 💬 [Matrix](https://matrix.to/#/#redlib:matrix.org), 🐋 [Quay.io](https://quay.io/repository/redlib/redlib), :octocat: [GitHub](https://github.com/redlib-org/redlib), and 🦊 [GitLab](https://gitlab.com/redlib/redlib).
|
||||
Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Redlib was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||
|
||||
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues).
|
||||
|
||||
## Built with
|
||||
|
||||
@ -36,42 +63,51 @@ Find Redlib on 💬 [Matrix](https://matrix.to/#/#redlib:matrix.org), 🐋 [Quay
|
||||
- [Askama](https://github.com/djc/askama) - Templating engine
|
||||
- [Rustls](https://github.com/rustls/rustls) - TLS library
|
||||
|
||||
## Info
|
||||
Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Redlib was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||
## How is it different from other Reddit front ends?
|
||||
|
||||
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues).
|
||||
|
||||
## How does it compare to Teddit?
|
||||
### Teddit
|
||||
|
||||
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two, and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Redlib into an even more polished product.
|
||||
|
||||
If you are looking to compare, the biggest differences I have noticed are:
|
||||
|
||||
- Redlib is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
||||
- Redlib is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation.
|
||||
|
||||
### Libreddit
|
||||
|
||||
While originating as a fork of Libreddit, the name "Redlib" was adopted to avoid legal issues, as Reddit only allows the use of their name if structured as "XYZ For Reddit".
|
||||
|
||||
Several technical improvements have also been made, including:
|
||||
|
||||
- **OAuth token spoofing**: To circumvent rate limits imposed by Reddit, OAuth token spoofing is used to mimick the most common iOS and Android clients. While spoofing both iOS and Android clients was explored, only the Android client was chosen due to content restrictions when using an anonymous iOS client.
|
||||
- **Token refreshing**: The authentication token is refreshed every 24 hours, emulating the behavior of the official Android app.
|
||||
- **HTTP header mimicking**: Efforts are made to send along as many of the official app's headers as possible to reduce the likelihood of Reddit's crackdown on Redlib's requests.
|
||||
|
||||
---
|
||||
|
||||
# Comparison
|
||||
|
||||
This section outlines how Redlib compares to Reddit.
|
||||
This section outlines how Redlib compares to Reddit in terms of speed and privacy.
|
||||
|
||||
## Speed
|
||||
|
||||
Lasted tested Jan 12, 2024.
|
||||
Last tested on January 12, 2024.
|
||||
|
||||
Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.dev/report?url=https%3A%2F%2Fredlib.matthew.science%2F), [Reddit Report](https://pagespeed.web.dev/report?url=https://www.reddit.com)).
|
||||
|
||||
| | Redlib | Reddit |
|
||||
|------------------------|-------------|-----------|
|
||||
| Speed Index | 0.6s | 1.9s |
|
||||
| Performance Score | *100%* | *64%* |
|
||||
| Time to Interactive | **2.8s** | **12.4s** |
|
||||
| Performance metric | Redlib | Reddit |
|
||||
| ------------------- | -------- | --------- |
|
||||
| Speed Index | 0.6s | 1.9s |
|
||||
| Performance Score | 100% | 64% |
|
||||
| Time to Interactive | **2.8s** | **12.4s** |
|
||||
|
||||
## Privacy
|
||||
|
||||
### Reddit
|
||||
|
||||
**Logging:** According to Reddit's [privacy policy](https://www.redditinc.com/policies/privacy-policy), they "may [automatically] log information" including:
|
||||
|
||||
- IP address
|
||||
- User-agent string
|
||||
- Browser type
|
||||
@ -85,12 +121,14 @@ Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.de
|
||||
- Search terms
|
||||
|
||||
**Location:** The same privacy policy goes on to describe that location data may be collected through the use of:
|
||||
|
||||
- GPS (consensual)
|
||||
- Bluetooth (consensual)
|
||||
- Content associated with a location (consensual)
|
||||
- Your IP Address
|
||||
|
||||
**Cookies:** Reddit's [cookie notice](https://www.redditinc.com/policies/cookies) documents the array of cookies used by Reddit including/regarding:
|
||||
|
||||
- Authentication
|
||||
- Functionality
|
||||
- Analytics and Performance
|
||||
@ -104,155 +142,111 @@ For transparency, I hope to describe all the ways Redlib handles user privacy.
|
||||
|
||||
#### Server
|
||||
|
||||
* **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting.
|
||||
- **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting.
|
||||
|
||||
* **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://redlib.matthew.science/settings). These are not cross-site cookies and the cookies hold no personal data.
|
||||
|
||||
#### Official instance (redlib.matthew.science)
|
||||
|
||||
The official instance is hosted at https://redlib.matthew.science.
|
||||
|
||||
* **Server:** The official instance runs a production binary, and thus logs nothing.
|
||||
|
||||
* **DNS:** The domain for the official instance uses Cloudflare as the DNS resolver. However, this site is not proxied through Cloudflare, and thus Cloudflare doesn't have access to user traffic.
|
||||
|
||||
* **Hosting:** The official instance is hosted on [Replit](https://replit.com/), which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models, and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
|
||||
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
<!-- ## 1) Cargo
|
||||
|
||||
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
|
||||
|
||||
```
|
||||
cargo install libreddit
|
||||
``` -->
|
||||
|
||||
## 2) Docker
|
||||
|
||||
Deploy the [Docker image](https://quay.io/repository/redlib/redlib) of Redlib:
|
||||
```
|
||||
docker pull quay.io/redlib/redlib
|
||||
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib
|
||||
```
|
||||
|
||||
Deploy using a different port (in this case, port 80):
|
||||
```
|
||||
docker pull quay.io/redlib/redlib
|
||||
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib
|
||||
```
|
||||
|
||||
To deploy on `arm64` platforms, simply replace `quay.io/redlib/redlib` in the commands above with `quay.io/redlib/redlib:latest-arm`.
|
||||
|
||||
To deploy on `armv7` platforms, simply replace `quay.io/redlib/redlib` in the commands above with `quay.io/redlib/redlib:latest-armv7`.
|
||||
|
||||
<!-- ## 3) AUR
|
||||
|
||||
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
|
||||
|
||||
```
|
||||
yay -S libreddit-git
|
||||
```
|
||||
## 4) NetBSD/pkgsrc
|
||||
|
||||
For NetBSD users, Redlib is available from the official repositories.
|
||||
|
||||
```
|
||||
pkgin install libreddit
|
||||
```
|
||||
|
||||
Or, if you prefer to build from source
|
||||
|
||||
```
|
||||
cd /usr/pkgsrc/libreddit
|
||||
make install
|
||||
``` -->
|
||||
|
||||
## 5) GitHub Releases
|
||||
|
||||
If you're on Linux and none of these methods work for you, you can grab a Linux binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest).
|
||||
|
||||
## 6) Replit/Heroku/Glitch
|
||||
|
||||
> **Warning**
|
||||
> These are free hosting options, but they are *not* private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
|
||||
|
||||
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
|
||||
[](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
|
||||
- **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://redlib.matthew.science/settings). These are not cross-site cookies and the cookies hold no personal data.
|
||||
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
Once installed, deploy Redlib to `0.0.0.0:8080` by running:
|
||||
This section covers multiple ways of deploying Redlib. Using [Docker](#docker) is recommended for production.
|
||||
|
||||
For configuration options, see the [Configuration section](#Configuration).
|
||||
|
||||
## Docker
|
||||
|
||||
[Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host.
|
||||
|
||||
Docker images for Redlib are available at [quay.io](https://quay.io/repository/redlib/redlib), with support for `amd64`, `arm64`, and `armv7` platforms.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
> [!IMPORTANT]
|
||||
> These instructions assume the [Compose plugin](https://docs.docker.com/compose/migrate/#what-are-the-differences-between-compose-v1-and-compose-v2) has already been installed. If not, follow these [instructions on the Docker Docs](https://docs.docker.com/compose/install) for how to do so.
|
||||
|
||||
Copy `compose.yaml` and modify any relevant values (for example, the ports Redlib should listen on).
|
||||
|
||||
Start Redlib in detached mode (running in the background):
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Stream logs from the Redlib container:
|
||||
|
||||
```bash
|
||||
docker logs -f redlib
|
||||
```
|
||||
|
||||
### Docker CLI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If deploying on:
|
||||
>
|
||||
> - an `arm64` platform, use the `quay.io/redlib/redlib:latest-arm` image instead.
|
||||
> - an `armv7` platform, use the `quay.io/redlib/redlib:latest-armv7` image instead.
|
||||
|
||||
Deploy Redlib:
|
||||
|
||||
```bash
|
||||
docker pull quay.io/redlib/redlib:latest
|
||||
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib:latest
|
||||
```
|
||||
|
||||
Deploy using a different port on the host (in this case, port 80):
|
||||
|
||||
```bash
|
||||
docker pull quay.io/redlib/redlib:latest
|
||||
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib:latest
|
||||
```
|
||||
|
||||
If you're using a reverse proxy in front of Redlib, prefix the port numbers with `127.0.0.1` so that Redlib only listens on the host port **locally**. For example, if the host port for Redlib is `8080`, specify `127.0.0.1:8080:8080`.
|
||||
|
||||
Stream logs from the Redlib container:
|
||||
|
||||
```bash
|
||||
docker logs -f redlib
|
||||
```
|
||||
|
||||
## Binary
|
||||
|
||||
If you're on Linux, you can grab a binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest) from GitHub.
|
||||
|
||||
Download the binary using [Wget](https://www.gnu.org/software/wget/):
|
||||
|
||||
```bash
|
||||
wget https://github.com/redlib-org/redlib/releases/download/v0.31.0/redlib
|
||||
```
|
||||
|
||||
Make the binary executable and change its ownership to `root`:
|
||||
|
||||
```bash
|
||||
sudo chmod +x redlib && sudo chown root:root redlib
|
||||
```
|
||||
|
||||
Copy the binary to `/usr/bin`:
|
||||
|
||||
```bash
|
||||
sudo cp ./redlib /usr/bin/redlib
|
||||
```
|
||||
|
||||
Deploy Redlib to `0.0.0.0:8080`:
|
||||
|
||||
```bash
|
||||
redlib
|
||||
```
|
||||
|
||||
## Instance settings
|
||||
|
||||
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
||||
|
||||
| Name | Possible values | Default value | Description |
|
||||
|---------------------------|-----------------|------------------|-----------------------------------------------------------------------------------------------------------|
|
||||
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
|
||||
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
|
||||
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
|
||||
| `PUSHSHIFT_FRONTEND` | String | `www.unddit.com` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
|
||||
|
||||
## Default User Settings
|
||||
|
||||
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
|
||||
|
||||
| Name | Possible values | Default value |
|
||||
|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|---------------|
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||
| `WIDE` | `["on", "off"]` | `off` |
|
||||
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||
| `BLUR_NSFW` | `["on", "off"]` | `off` |
|
||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
|
||||
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
|
||||
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
|
||||
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
|
||||
| `HIDE_SCORE` | `["on", "off"]` | `off` |
|
||||
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |
|
||||
|
||||
You can also configure Redlib with a configuration file. An example `redlib.toml` can be found below:
|
||||
|
||||
```toml
|
||||
REDLIB_DEFAULT_WIDE = "on"
|
||||
REDLIB_DEFAULT_USE_HLS = "on"
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
REDLIB_DEFAULT_SHOW_NSFW=on redlib
|
||||
```
|
||||
|
||||
```bash
|
||||
REDLIB_DEFAULT_WIDE=on REDLIB_DEFAULT_THEME=dark redlib -r
|
||||
```
|
||||
|
||||
## Proxying using NGINX
|
||||
|
||||
> **Note**
|
||||
> If you're [proxying Redlib through an NGINX Reverse Proxy](https://github.com/libreddit/libreddit/issues/122#issuecomment-782226853), add
|
||||
> [!IMPORTANT]
|
||||
> If you're proxying Redlib through NGINX (see [issue #122](https://github.com/libreddit/libreddit/issues/122#issuecomment-782226853)), add
|
||||
>
|
||||
> ```nginx
|
||||
> proxy_http_version 1.1;
|
||||
> ```
|
||||
>
|
||||
> to your NGINX configuration file above your `proxy_pass` line.
|
||||
|
||||
## systemd
|
||||
### Running as a systemd service
|
||||
|
||||
You can use the systemd service available in `contrib/redlib.service`
|
||||
(install it on `/etc/systemd/system/redlib.service`).
|
||||
@ -271,18 +265,126 @@ guarantee nginx waits for this service to start. Edit
|
||||
Before=nginx.service
|
||||
```
|
||||
|
||||
## launchd
|
||||
## Building from source
|
||||
|
||||
If you are on macOS, you can use the launchd service available in `contrib/redlib.plist`.
|
||||
To deploy Redlib with changes not yet included in the latest release, you can build the application from source.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/redlib-org/redlib && cd redlib
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Replit/Heroku
|
||||
|
||||
> [!WARNING]
|
||||
> These are free hosting options, but they are _not_ private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
|
||||
|
||||
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
|
||||
[](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
|
||||
|
||||
## launchd (macOS)
|
||||
|
||||
If you are on macOS, you can use the [launchd](https://en.wikipedia.org/wiki/Launchd) service available in `contrib/redlib.plist`.
|
||||
|
||||
Install it with `cp contrib/redlib.plist ~/Library/LaunchAgents/`.
|
||||
|
||||
Load and start it with `launchctl load ~/Library/LaunchAgents/redlib.plist`.
|
||||
|
||||
## Building
|
||||
<!-- ## Cargo
|
||||
|
||||
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
|
||||
|
||||
```bash
|
||||
cargo install libreddit
|
||||
``` -->
|
||||
|
||||
<!-- ## AUR
|
||||
|
||||
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
|
||||
|
||||
```bash
|
||||
yay -S libreddit-git
|
||||
```
|
||||
git clone https://github.com/redlib-org/redlib
|
||||
cd redlib
|
||||
cargo run
|
||||
## NetBSD/pkgsrc
|
||||
|
||||
For NetBSD users, Redlib is available from the official repositories.
|
||||
|
||||
```bash
|
||||
pkgin install libreddit
|
||||
```
|
||||
|
||||
Or, if you prefer to build from source
|
||||
|
||||
```bash
|
||||
cd /usr/pkgsrc/libreddit
|
||||
make install
|
||||
``` -->
|
||||
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
You can configure Redlib further using environment variables. For example:
|
||||
|
||||
```bash
|
||||
REDLIB_DEFAULT_SHOW_NSFW=on redlib
|
||||
```
|
||||
|
||||
```bash
|
||||
REDLIB_DEFAULT_WIDE=on REDLIB_DEFAULT_THEME=dark redlib -r
|
||||
```
|
||||
|
||||
You can also configure Redlib with a configuration file named `redlib.toml`. For example:
|
||||
|
||||
```toml
|
||||
REDLIB_DEFAULT_WIDE = "on"
|
||||
REDLIB_DEFAULT_USE_HLS = "on"
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you're deploying Redlib using the **Docker CLI or Docker Compose**, environment variables can be defined in a [`.env` file](https://docs.docker.com/compose/environment-variables/set-environment-variables/), allowing you to centralize and manage configuration in one place.
|
||||
>
|
||||
> To configure Redlib using a `.env` file, copy the `.env.example` file to `.env` and edit it accordingly.
|
||||
>
|
||||
> If using the Docker CLI, add ` --env-file .env` to the command that runs Redlib. For example:
|
||||
>
|
||||
> ```bash
|
||||
> docker run -d --name redlib -p 8080:8080 --env-file .env quay.io/redlib/redlib:latest
|
||||
> ```
|
||||
>
|
||||
> If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line.
|
||||
|
||||
## Instance settings
|
||||
|
||||
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
||||
|
||||
| Name | Possible values | Default value | Description |
|
||||
| ------------------------- | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
|
||||
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
|
||||
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
|
||||
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
|
||||
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
|
||||
|
||||
## Default user settings
|
||||
|
||||
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
|
||||
|
||||
| Name | Possible values | Default value |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||
| `WIDE` | `["on", "off"]` | `off` |
|
||||
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||
| `BLUR_NSFW` | `["on", "off"]` | `off` |
|
||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
|
||||
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
|
||||
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
|
||||
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
|
||||
| `HIDE_SCORE` | `["on", "off"]` | `off` |
|
||||
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |
|
||||
|
@ -2,26 +2,25 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
redsunlib:
|
||||
build: .
|
||||
restart: always
|
||||
container_name: "redlib"
|
||||
container_name: "redsunlib"
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 8080:8080 # Specify `127.0.0.1:8080:8080` instead if using a reverse proxy
|
||||
user: nobody
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# - seccomp=seccomp-redsunlib.json
|
||||
cap_drop:
|
||||
- ALL
|
||||
networks:
|
||||
- redlib
|
||||
security_opt:
|
||||
- seccomp="seccomp-redlib.json"
|
||||
- redsunlib
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
|
||||
interval: 5m
|
||||
timeout: 3s
|
||||
|
||||
networks:
|
||||
redlib:
|
||||
redsunlib:
|
@ -1,28 +1,24 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: quay.io/redlib/redlib
|
||||
# quay.io/redlib/redlib:latest-arm # uncomment if you use arm64
|
||||
# quay.io/redlib/redlib:latest-armv7 # uncomment if you use armv7
|
||||
redsunlib:
|
||||
image: git.stardust.wtf/iridium/redsunlib:latest
|
||||
restart: always
|
||||
container_name: "redlib"
|
||||
container_name: "redsunlib"
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 8080:8080 # Specify `127.0.0.1:8080:8080` instead if using a reverse proxy
|
||||
user: nobody
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# - seccomp=seccomp-redsunlib.json
|
||||
cap_drop:
|
||||
- ALL
|
||||
env_file: .env
|
||||
networks:
|
||||
- redlib
|
||||
security_opt:
|
||||
- seccomp="seccomp-redlib.json"
|
||||
- redsunlib
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
|
||||
interval: 5m
|
||||
timeout: 3s
|
||||
|
||||
networks:
|
||||
redlib:
|
||||
redsunlib:
|
@ -21,7 +21,12 @@ use crate::utils::format_url;
|
||||
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
||||
|
||||
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.expect("No native root certificates found")
|
||||
.https_only()
|
||||
.enable_http1()
|
||||
.build();
|
||||
client::Client::builder().build(https)
|
||||
});
|
||||
|
||||
@ -165,7 +170,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
// Construct the hyper client from the HTTPS connector.
|
||||
let client: Client<_, Body> = CLIENT.clone();
|
||||
|
||||
let (token, vendor_id, device_id, user_agent, loid) = {
|
||||
let (token, vendor_id, device_id, mut user_agent, loid) = {
|
||||
let client = block_on(OAUTH_CLIENT.read());
|
||||
(
|
||||
client.token.clone(),
|
||||
@ -175,6 +180,14 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(),
|
||||
)
|
||||
};
|
||||
|
||||
// Replace "Android" with a tricky word.
|
||||
// Issues: #78/#115, #116
|
||||
// If you include the word "Android", you will get a number of different errors
|
||||
// I guess they don't expect mobile traffic on the endpoints we use
|
||||
// Scrawled on wall for next poor soul: Run the test suite.
|
||||
user_agent = user_agent.replace("Android", "Andr\u{200B}oid");
|
||||
|
||||
// Build request to Reddit. When making a GET, request gzip compression.
|
||||
// (Reddit doesn't do brotli yet.)
|
||||
let builder = Request::builder()
|
||||
@ -320,24 +333,19 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
let json: Value = value;
|
||||
// If Reddit returned an error
|
||||
if json["error"].is_i64() {
|
||||
Err(
|
||||
json["reason"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| {
|
||||
json["message"].as_str().unwrap_or_else(|| {
|
||||
eprintln!("{REDDIT_URL_BASE}{path} - Error parsing reddit error");
|
||||
"Error parsing reddit error"
|
||||
})
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
// OAuth token has expired; http status 401
|
||||
if json["message"] == "Unauthorized" {
|
||||
error!("Forcing a token refresh");
|
||||
let () = force_refresh_token().await;
|
||||
return Err("OAuth token has expired. Please refresh the page!".to_string());
|
||||
}
|
||||
Err(format!("Reddit error {} \"{}\": {}", json["error"], json["reason"], json["message"]))
|
||||
} else {
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Got a bad response from reddit {e} - forcing a token refresh. Status code: {status}");
|
||||
let _ = force_refresh_token().await;
|
||||
error!("Got an invalid response from reddit {e}. Status code: {status}");
|
||||
if status.is_server_error() {
|
||||
Err("Reddit is having issues, check if there's an outage".to_string())
|
||||
} else {
|
||||
|
@ -9,9 +9,9 @@ use std::{env::var, fs::read_to_string};
|
||||
// first request) and contains the instance settings.
|
||||
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
|
||||
|
||||
// This serves as the frontend for the Pushshift API - on removed comments, this URL will
|
||||
// be the base of a link, to display removed content (on another site).
|
||||
pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "www.unddit.com";
|
||||
// This serves as the frontend for an archival API - on removed comments, this URL
|
||||
// will be the base of a link, to display removed content (on another site).
|
||||
pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "undelete.pullpush.io";
|
||||
|
||||
/// Stores the configuration parsed from the environment variables and the
|
||||
/// config file. `Config::Default()` contains None for each setting.
|
||||
@ -60,6 +60,10 @@ pub struct Config {
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_USE_HLS")]
|
||||
pub(crate) default_use_hls: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_FFMPEG_VIDEO_DOWNLOADS")]
|
||||
pub(crate) default_ffmpeg_video_downloads: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
|
||||
pub(crate) default_hide_hls_notification: Option<String>,
|
||||
@ -68,6 +72,10 @@ pub struct Config {
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
|
||||
pub(crate) default_hide_awards: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
|
||||
pub(crate) default_hide_sidebar_and_summary: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
|
||||
pub(crate) default_hide_score: Option<String>,
|
||||
@ -125,8 +133,10 @@ impl Config {
|
||||
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
|
||||
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
|
||||
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
|
||||
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS"),
|
||||
default_ffmpeg_video_downloads: parse("REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS"),
|
||||
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION"),
|
||||
default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"),
|
||||
default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
|
||||
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
|
||||
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
|
||||
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
|
||||
@ -148,9 +158,11 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
|
||||
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
|
||||
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
|
||||
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
|
||||
"REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS" => config.default_ffmpeg_video_downloads.clone(),
|
||||
"REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
|
||||
"REDLIB_DEFAULT_WIDE" => config.default_wide.clone(),
|
||||
"REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(),
|
||||
"REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_awards.clone(),
|
||||
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
|
||||
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
|
||||
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
|
||||
|
@ -151,7 +151,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
}
|
||||
|
||||
if have_after {
|
||||
before = "t3_".to_owned();
|
||||
"t3_".clone_into(&mut before);
|
||||
before.push_str(&duplicates[0].id);
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
if have_before {
|
||||
// The next batch will need to start from one after the
|
||||
// last post in the current batch.
|
||||
after = "t3_".to_owned();
|
||||
"t3_".clone_into(&mut after);
|
||||
after.push_str(&duplicates[l - 1].id);
|
||||
|
||||
// Here is where things get terrible. Notice that we
|
||||
@ -182,7 +182,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
match json(new_path, true).await {
|
||||
Ok(response) => {
|
||||
if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() {
|
||||
before = "t3_".to_owned();
|
||||
"t3_".clone_into(&mut before);
|
||||
before.push_str(&duplicates[0].id);
|
||||
}
|
||||
}
|
||||
|
79
src/main.rs
@ -1,42 +1,6 @@
|
||||
// Global specifiers
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
anonymous_parameters,
|
||||
clippy::all,
|
||||
illegal_floating_point_literal_pattern,
|
||||
late_bound_lifetime_arguments,
|
||||
path_statements,
|
||||
patterns_in_fns_without_body,
|
||||
rust_2018_idioms,
|
||||
trivial_numeric_casts,
|
||||
unused_extern_crates
|
||||
)]
|
||||
#![warn(
|
||||
clippy::dbg_macro,
|
||||
clippy::decimal_literal_representation,
|
||||
clippy::get_unwrap,
|
||||
clippy::nursery,
|
||||
clippy::pedantic,
|
||||
clippy::todo,
|
||||
clippy::unimplemented,
|
||||
clippy::use_debug,
|
||||
clippy::all,
|
||||
unused_qualifications,
|
||||
variant_size_differences
|
||||
)]
|
||||
#![allow(
|
||||
clippy::cmp_owned,
|
||||
clippy::unused_async,
|
||||
clippy::option_if_let_else,
|
||||
clippy::items_after_statements,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::struct_field_names,
|
||||
clippy::struct_excessive_bools,
|
||||
clippy::useless_let_if_seq,
|
||||
clippy::collection_is_never_read
|
||||
)]
|
||||
#![allow(clippy::cmp_owned)]
|
||||
|
||||
// Reference local files
|
||||
mod config;
|
||||
@ -114,6 +78,17 @@ async fn font() -> Result<Response<Body>, String> {
|
||||
)
|
||||
}
|
||||
|
||||
async fn ffmpeg() -> Result<Response<Body>, String> {
|
||||
Ok(
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("content-type", "application/wasm")
|
||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||
.body(include_bytes!("../static/ffmpeg/ffmpeg-core.wasm").as_ref().into())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
|
||||
let mut res = Response::builder()
|
||||
.status(200)
|
||||
@ -202,7 +177,7 @@ async fn main() {
|
||||
|
||||
let listener = [address, ":", port].concat();
|
||||
|
||||
println!("Starting Redlib...");
|
||||
println!("Starting Redsunlib...");
|
||||
|
||||
// Begin constructing a server
|
||||
let mut app = server::Server::new();
|
||||
@ -225,7 +200,7 @@ async fn main() {
|
||||
"Referrer-Policy" => "no-referrer",
|
||||
"X-Content-Type-Options" => "nosniff",
|
||||
"X-Frame-Options" => "DENY",
|
||||
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src blob:;"
|
||||
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src 'self' blob:;"
|
||||
};
|
||||
|
||||
if let Some(expire_time) = hsts {
|
||||
@ -260,14 +235,34 @@ async fn main() {
|
||||
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
|
||||
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
|
||||
app
|
||||
.at("/playHLSVideo.js")
|
||||
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed());
|
||||
.at("/videoUtils.js")
|
||||
.get(|_| resource(include_str!("../static/videoUtils.js"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/hls.min.js")
|
||||
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/highlighted.js")
|
||||
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed());
|
||||
// FFmpeg
|
||||
app
|
||||
.at("/ffmpeg/814.ffmpeg.js")
|
||||
.get(|_| resource(include_str!("../static/ffmpeg/814.ffmpeg.js"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/ffmpeg/814.ffmpeg.js.map")
|
||||
.get(|_| resource(include_str!("../static/ffmpeg/814.ffmpeg.js.map"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/ffmpeg/ffmpeg-core.js")
|
||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg-core.js"), "text/javascript", false).boxed());
|
||||
app.at("/ffmpeg/ffmpeg-core.wasm").get(|_| ffmpeg().boxed());
|
||||
app
|
||||
.at("/ffmpeg/ffmpeg-util.js")
|
||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg-util.js"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/ffmpeg/ffmpeg.js")
|
||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg.js"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/ffmpeg/ffmpeg.js.map")
|
||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg.js.map"), "text/javascript", false).boxed());
|
||||
|
||||
// Proxy media through Redlib
|
||||
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
|
||||
@ -409,7 +404,7 @@ async fn main() {
|
||||
// Default service in case no routes match
|
||||
app.at("/*").get(|req| error(req, "Nothing here").boxed());
|
||||
|
||||
println!("Running Redlib v{} on {listener}!", env!("CARGO_PKG_VERSION"));
|
||||
println!("Running Redsunlib v{} on {listener}!", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let server = app.listen(&listener);
|
||||
|
||||
|
@ -4,6 +4,44 @@
|
||||
// Filled in with real app versions
|
||||
pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""];
|
||||
pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||
"Version 2023.48.0/Build 1319123",
|
||||
"Version 2023.49.0/Build 1321715",
|
||||
"Version 2023.49.1/Build 1322281",
|
||||
"Version 2023.50.0/Build 1332338",
|
||||
"Version 2023.50.1/Build 1345844",
|
||||
"Version 2024.02.0/Build 1368985",
|
||||
"Version 2024.03.0/Build 1379408",
|
||||
"Version 2024.04.0/Build 1391236",
|
||||
"Version 2024.05.0/Build 1403584",
|
||||
"Version 2024.06.0/Build 1418489",
|
||||
"Version 2024.07.0/Build 1429651",
|
||||
"Version 2024.08.0/Build 1439531",
|
||||
"Version 2024.10.0/Build 1470045",
|
||||
"Version 2024.10.1/Build 1478645",
|
||||
"Version 2024.11.0/Build 1480707",
|
||||
"Version 2024.12.0/Build 1494694",
|
||||
"Version 2024.13.0/Build 1505187",
|
||||
"Version 2024.14.0/Build 1520556",
|
||||
"Version 2024.15.0/Build 1536823",
|
||||
"Version 2024.16.0/Build 1551366",
|
||||
"Version 2024.17.0/Build 1568106",
|
||||
"Version 2024.18.0/Build 1577901",
|
||||
"Version 2024.18.1/Build 1585304",
|
||||
"Version 2024.19.0/Build 1593346",
|
||||
"Version 2024.20.0/Build 1612800",
|
||||
"Version 2024.20.1/Build 1615586",
|
||||
"Version 2024.20.2/Build 1624969",
|
||||
"Version 2024.21.0/Build 1631686",
|
||||
"Version 2024.22.0/Build 1645257",
|
||||
"Version 2024.22.1/Build 1652272",
|
||||
"Version 2023.21.0/Build 956283",
|
||||
"Version 2023.22.0/Build 968223",
|
||||
"Version 2023.23.0/Build 983896",
|
||||
"Version 2023.24.0/Build 998541",
|
||||
"Version 2023.25.0/Build 1014750",
|
||||
"Version 2023.25.1/Build 1018737",
|
||||
"Version 2023.26.0/Build 1019073",
|
||||
"Version 2023.27.0/Build 1031923",
|
||||
"Version 2023.28.0/Build 1046887",
|
||||
"Version 2023.29.0/Build 1059855",
|
||||
"Version 2023.30.0/Build 1078734",
|
||||
@ -26,14 +64,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||
"Version 2023.44.0/Build 1268622",
|
||||
"Version 2023.45.0/Build 1281371",
|
||||
"Version 2023.47.0/Build 1303604",
|
||||
"Version 2023.48.0/Build 1319123",
|
||||
"Version 2023.49.0/Build 1321715",
|
||||
"Version 2023.49.1/Build 1322281",
|
||||
"Version 2023.50.0/Build 1332338",
|
||||
"Version 2023.50.1/Build 1345844",
|
||||
"Version 2024.02.0/Build 1368985",
|
||||
"Version 2024.03.0/Build 1379408",
|
||||
"Version 2024.04.0/Build 1391236",
|
||||
"Version 2022.42.0/Build 638508",
|
||||
"Version 2022.43.0/Build 648277",
|
||||
"Version 2022.44.0/Build 664348",
|
||||
"Version 2022.45.0/Build 677985",
|
||||
"Version 2023.01.0/Build 709875",
|
||||
"Version 2023.02.0/Build 717912",
|
||||
"Version 2023.03.0/Build 729220",
|
||||
"Version 2023.04.0/Build 744681",
|
||||
"Version 2023.05.0/Build 755453",
|
||||
"Version 2023.06.0/Build 775017",
|
||||
"Version 2023.07.0/Build 788827",
|
||||
@ -56,14 +94,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||
"Version 2023.19.0/Build 927681",
|
||||
"Version 2023.20.0/Build 943980",
|
||||
"Version 2023.20.1/Build 946732",
|
||||
"Version 2023.21.0/Build 956283",
|
||||
"Version 2023.22.0/Build 968223",
|
||||
"Version 2023.23.0/Build 983896",
|
||||
"Version 2023.24.0/Build 998541",
|
||||
"Version 2023.25.0/Build 1014750",
|
||||
"Version 2023.25.1/Build 1018737",
|
||||
"Version 2023.26.0/Build 1019073",
|
||||
"Version 2023.27.0/Build 1031923",
|
||||
"Version 2022.20.0/Build 487703",
|
||||
"Version 2022.21.0/Build 492436",
|
||||
"Version 2022.22.0/Build 498700",
|
||||
"Version 2022.23.0/Build 502374",
|
||||
"Version 2022.23.1/Build 506606",
|
||||
"Version 2022.24.0/Build 510950",
|
||||
"Version 2022.24.1/Build 513462",
|
||||
"Version 2022.25.0/Build 515072",
|
||||
"Version 2022.25.1/Build 516394",
|
||||
"Version 2022.25.2/Build 519915",
|
||||
"Version 2022.26.0/Build 521193",
|
||||
@ -86,14 +124,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||
"Version 2022.40.0/Build 624782",
|
||||
"Version 2022.41.0/Build 630468",
|
||||
"Version 2022.41.1/Build 634168",
|
||||
"Version 2022.42.0/Build 638508",
|
||||
"Version 2022.43.0/Build 648277",
|
||||
"Version 2022.44.0/Build 664348",
|
||||
"Version 2022.45.0/Build 677985",
|
||||
"Version 2023.01.0/Build 709875",
|
||||
"Version 2023.02.0/Build 717912",
|
||||
"Version 2023.03.0/Build 729220",
|
||||
"Version 2023.04.0/Build 744681",
|
||||
"Version 2021.39.1/Build 372418",
|
||||
"Version 2021.41.0/Build 376052",
|
||||
"Version 2021.42.0/Build 378193",
|
||||
"Version 2021.43.0/Build 382019",
|
||||
"Version 2021.44.0/Build 385129",
|
||||
"Version 2021.45.0/Build 387663",
|
||||
"Version 2021.46.0/Build 392043",
|
||||
"Version 2021.47.0/Build 394342",
|
||||
"Version 2022.10.0/Build 429896",
|
||||
"Version 2022.1.0/Build 402829",
|
||||
"Version 2022.11.0/Build 433004",
|
||||
@ -106,15 +144,7 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||
"Version 2022.17.0/Build 468480",
|
||||
"Version 2022.18.0/Build 473740",
|
||||
"Version 2022.19.1/Build 482464",
|
||||
"Version 2022.20.0/Build 487703",
|
||||
"Version 2022.2.0/Build 405543",
|
||||
"Version 2022.21.0/Build 492436",
|
||||
"Version 2022.22.0/Build 498700",
|
||||
"Version 2022.23.0/Build 502374",
|
||||
"Version 2022.23.1/Build 506606",
|
||||
"Version 2022.24.0/Build 510950",
|
||||
"Version 2022.24.1/Build 513462",
|
||||
"Version 2022.25.0/Build 515072",
|
||||
"Version 2022.3.0/Build 408637",
|
||||
"Version 2022.4.0/Build 411368",
|
||||
"Version 2022.5.0/Build 414731",
|
||||
@ -124,35 +154,5 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||
"Version 2022.7.0/Build 420849",
|
||||
"Version 2022.8.0/Build 423906",
|
||||
"Version 2022.9.0/Build 426592",
|
||||
"Version 2021.20.0/Build 326964",
|
||||
"Version 2021.21.0/Build 327703",
|
||||
"Version 2021.21.1/Build 328461",
|
||||
"Version 2021.22.0/Build 329696",
|
||||
"Version 2021.23.0/Build 331631",
|
||||
"Version 2021.24.0/Build 333951",
|
||||
"Version 2021.25.0/Build 335451",
|
||||
"Version 2021.26.0/Build 336739",
|
||||
"Version 2021.27.0/Build 338857",
|
||||
"Version 2021.28.0/Build 340747",
|
||||
"Version 2021.29.0/Build 342342",
|
||||
"Version 2021.30.0/Build 343820",
|
||||
"Version 2021.31.0/Build 346485",
|
||||
"Version 2021.32.0/Build 349507",
|
||||
"Version 2021.33.0/Build 351843",
|
||||
"Version 2021.34.0/Build 353911",
|
||||
"Version 2021.35.0/Build 355878",
|
||||
"Version 2021.36.0/Build 359254",
|
||||
"Version 2021.36.1/Build 360572",
|
||||
"Version 2021.37.0/Build 361905",
|
||||
"Version 2021.38.0/Build 365032",
|
||||
"Version 2021.39.0/Build 369068",
|
||||
"Version 2021.39.1/Build 372418",
|
||||
"Version 2021.41.0/Build 376052",
|
||||
"Version 2021.42.0/Build 378193",
|
||||
"Version 2021.43.0/Build 382019",
|
||||
"Version 2021.44.0/Build 385129",
|
||||
"Version 2021.45.0/Build 387663",
|
||||
"Version 2021.46.0/Build 392043",
|
||||
"Version 2021.47.0/Build 394342",
|
||||
];
|
||||
pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""];
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use brotli::enc::{BrotliCompress, BrotliEncoderParams};
|
||||
use cached::proc_macro::cached;
|
||||
use cookie::Cookie;
|
||||
@ -15,6 +17,7 @@ use libflate::gzip;
|
||||
use route_recognizer::{Params, Router};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::Display,
|
||||
io,
|
||||
pin::Pin,
|
||||
result::Result,
|
||||
@ -65,12 +68,12 @@ impl CompressionType {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for CompressionType {
|
||||
fn to_string(&self) -> String {
|
||||
impl Display for CompressionType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Gzip => "gzip".to_string(),
|
||||
Self::Brotli => "br".to_string(),
|
||||
Self::Passthrough => String::new(),
|
||||
Self::Gzip => write!(f, "gzip"),
|
||||
Self::Brotli => write!(f, "br"),
|
||||
Self::Passthrough => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ struct SettingsTemplate {
|
||||
|
||||
// CONSTANTS
|
||||
|
||||
const PREFS: [&str; 15] = [
|
||||
const PREFS: [&str; 17] = [
|
||||
"theme",
|
||||
"front_page",
|
||||
"layout",
|
||||
@ -29,8 +29,10 @@ const PREFS: [&str; 15] = [
|
||||
"show_nsfw",
|
||||
"blur_nsfw",
|
||||
"use_hls",
|
||||
"ffmpeg_video_downloads",
|
||||
"hide_hls_notification",
|
||||
"autoplay_videos",
|
||||
"hide_sidebar_and_summary",
|
||||
"fixed_navbar",
|
||||
"hide_awards",
|
||||
"hide_score",
|
||||
@ -80,7 +82,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
Some(value) => response.insert_cookie(
|
||||
Cookie::build((name.to_owned(), value.clone()))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.http_only(name != "ffmpeg_video_downloads")
|
||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||
.into(),
|
||||
),
|
||||
|
@ -7,6 +7,8 @@ use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
@ -50,10 +52,13 @@ struct WallTemplate {
|
||||
url: String,
|
||||
}
|
||||
|
||||
static GEO_FILTER_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"geo_filter=(?<region>\w+)").unwrap());
|
||||
|
||||
// SERVICES
|
||||
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
// Build Reddit API path
|
||||
let root = req.uri().path() == "/";
|
||||
let query = req.uri().query().unwrap_or_default().to_string();
|
||||
let subscribed = setting(&req, "subscriptions");
|
||||
let front_page = setting(&req, "front_page");
|
||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||
@ -107,7 +112,11 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
let mut params = String::from("&raw_json=1");
|
||||
if sub_name == "popular" {
|
||||
params.push_str("&geo_filter=GLOBAL");
|
||||
let geo_filter = match GEO_FILTER_MATCH.captures(&query) {
|
||||
Some(geo_filter) => geo_filter["region"].to_string(),
|
||||
None => "GLOBAL".to_owned(),
|
||||
};
|
||||
params.push_str(&format!("&geo_filter={geo_filter}"));
|
||||
}
|
||||
|
||||
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default());
|
||||
|
76
src/utils.rs
@ -577,7 +577,9 @@ pub struct Preferences {
|
||||
pub show_nsfw: String,
|
||||
pub blur_nsfw: String,
|
||||
pub hide_hls_notification: String,
|
||||
pub hide_sidebar_and_summary: String,
|
||||
pub use_hls: String,
|
||||
pub ffmpeg_video_downloads: String,
|
||||
pub autoplay_videos: String,
|
||||
pub fixed_navbar: String,
|
||||
pub disable_visit_reddit_confirmation: String,
|
||||
@ -611,8 +613,10 @@ impl Preferences {
|
||||
layout: setting(req, "layout"),
|
||||
wide: setting(req, "wide"),
|
||||
show_nsfw: setting(req, "show_nsfw"),
|
||||
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
|
||||
blur_nsfw: setting(req, "blur_nsfw"),
|
||||
use_hls: setting(req, "use_hls"),
|
||||
ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"),
|
||||
hide_hls_notification: setting(req, "hide_hls_notification"),
|
||||
autoplay_videos: setting(req, "autoplay_videos"),
|
||||
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
|
||||
@ -875,16 +879,18 @@ pub fn format_url(url: &str) -> String {
|
||||
|
||||
// These are links we want to replace in-body
|
||||
static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap());
|
||||
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview)\.redd\.it(.*)[^?]").unwrap());
|
||||
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)[^?]").unwrap());
|
||||
static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap());
|
||||
static REDLIB_PREVIEW_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"/(img|preview/)(pre|external-pre)?/(.*?)>"#).unwrap());
|
||||
static REDLIB_PREVIEW_TEXT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r">(.*?)</a>").unwrap());
|
||||
|
||||
// Rewrite Reddit links to Redlib in body of text
|
||||
pub fn rewrite_urls(input_text: &str) -> String {
|
||||
let text1 =
|
||||
let mut text1 =
|
||||
// Rewrite Reddit links to Redlib
|
||||
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
|
||||
.to_string();
|
||||
let text1 = REDDIT_EMOJI_REGEX
|
||||
text1 = REDDIT_EMOJI_REGEX
|
||||
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||
.to_string()
|
||||
// Remove (html-encoded) "\" from URLs.
|
||||
@ -892,12 +898,56 @@ pub fn rewrite_urls(input_text: &str) -> String {
|
||||
.replace("\\_", "_");
|
||||
|
||||
// Rewrite external media previews to Redlib
|
||||
if REDDIT_PREVIEW_REGEX.is_match(&text1) {
|
||||
REDDIT_PREVIEW_REGEX
|
||||
.replace_all(&text1, format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||
.to_string()
|
||||
} else {
|
||||
text1
|
||||
loop {
|
||||
if REDDIT_PREVIEW_REGEX.find(&text1).is_none() {
|
||||
return text1;
|
||||
} else {
|
||||
let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default());
|
||||
|
||||
let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
|
||||
let mut image_caption = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
|
||||
|
||||
/* As long as image_caption isn't empty remove first and last four characters of image_text to leave us with just the text in the caption without any HTML.
|
||||
This makes it possible to enclose it in a <figcaption> later on without having stray HTML breaking it */
|
||||
if !image_caption.is_empty() {
|
||||
image_caption = image_caption[1..image_caption.len() - 4].to_string();
|
||||
}
|
||||
|
||||
// image_url contains > at the end of it, and right above this we remove image_text's front >, leaving us with just a single > between them
|
||||
let image_to_replace = format!("<a href=\"{image_url}{image_caption}</a>");
|
||||
|
||||
// _image_replacement needs to be in scope for the replacement at the bottom of the loop
|
||||
let mut _image_replacement = String::new();
|
||||
|
||||
/* We don't want to show a caption that's just the image's link, so we check if we find a Reddit preview link within the image's caption.
|
||||
If we don't find one we must have actual text, so we include a <figcaption> block that contains it.
|
||||
Otherwise we don't include the <figcaption> block as we don't need it. */
|
||||
if REDDIT_PREVIEW_REGEX.find(&image_caption).is_none() {
|
||||
// Without this " would show as \" instead. "\"" is how the quotes are formatted within image_text beforehand
|
||||
image_caption = image_caption.replace("\\"", "\"");
|
||||
|
||||
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a><figcaption>{image_caption}</figcaption></figure>");
|
||||
} else {
|
||||
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a></figure>");
|
||||
}
|
||||
|
||||
/* In order to know if we're dealing with a normal or external preview we need to take a look at the first capture group of REDDIT_PREVIEW_REGEX
|
||||
if it's preview we're dealing with something that needs /preview/pre, external-preview is /preview/external-pre, and i is /img */
|
||||
let reddit_preview_regex_capture = REDDIT_PREVIEW_REGEX.captures(&text1).unwrap().get(1).map_or("", |m| m.as_str()).to_string();
|
||||
let mut _preview_type = String::new();
|
||||
if reddit_preview_regex_capture == "preview" {
|
||||
_preview_type = "/preview/pre".to_string();
|
||||
} else if reddit_preview_regex_capture == "external-preview" {
|
||||
_preview_type = "/preview/external-pre".to_string();
|
||||
} else {
|
||||
_preview_type = "/img".to_string();
|
||||
}
|
||||
|
||||
text1 = REDDIT_PREVIEW_REGEX
|
||||
.replace(&text1, format!("{_preview_type}$2"))
|
||||
.replace(&image_to_replace, &_image_replacement)
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1148,3 +1198,11 @@ async fn test_fetching_ws() {
|
||||
assert!(post.ws_url.starts_with("wss://k8s-lb.wss.redditmedia.com/link/"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewriting_image_links() {
|
||||
let input =
|
||||
r#"<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>"#;
|
||||
let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
|
||||
assert_eq!(rewrite_urls(input), output);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 969 B After Width: | Height: | Size: 4.3 KiB |
2
static/ffmpeg/814.ffmpeg.js
Normal file
@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{var e={454:e=>{function t(e){return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}t.keys=()=>[],t.resolve=t,t.id=454,e.exports=t}},t={};function r(a){var o=t[a];if(void 0!==o)return o.exports;var s=t[a]={exports:{}};return e[a](s,s.exports,r),s.exports}return r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(e||(e={}));const t=new Error("unknown message type"),a=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),o=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let s;self.onmessage=async({data:{id:n,type:E,data:i}})=>{const c=[];let p;try{if(E!==e.LOAD&&!s)throw a;switch(E){case e.LOAD:p=await(async({coreURL:t="https://unpkg.com/@ffmpeg/core@0.12.1/dist/umd/ffmpeg-core.js",wasmURL:a,workerURL:n})=>{const E=!s,i=t,c=a||t.replace(/.js$/g,".wasm"),p=n||t.replace(/.js$/g,".worker.js");try{importScripts(i)}catch{if(self.createFFmpegCore=(await r(454)(i)).default,!self.createFFmpegCore)throw o}return s=await self.createFFmpegCore({mainScriptUrlOrBlob:`${i}#${btoa(JSON.stringify({wasmURL:c,workerURL:p}))}`}),s.setLogger((t=>self.postMessage({type:e.LOG,data:t}))),s.setProgress((t=>self.postMessage({type:e.PROGRESS,data:t}))),E})(i);break;case e.EXEC:p=(({args:e,timeout:t=-1})=>{s.setTimeout(t),s.exec(...e);const r=s.ret;return s.reset(),r})(i);break;case e.WRITE_FILE:p=(({path:e,data:t})=>(s.FS.writeFile(e,t),!0))(i);break;case e.READ_FILE:p=(({path:e,encoding:t})=>s.FS.readFile(e,{encoding:t}))(i);break;case e.DELETE_FILE:p=(({path:e})=>(s.FS.unlink(e),!0))(i);break;case e.RENAME:p=(({oldPath:e,newPath:t})=>(s.FS.rename(e,t),!0))(i);break;case e.CREATE_DIR:p=(({path:e})=>(s.FS.mkdir(e),!0))(i);break;case e.LIST_DIR:p=(({path:e})=>{const t=s.FS.readdir(e),r=[];for(const a of t){const t=s.FS.stat(`${e}/${a}`),o=s.FS.isDir(t.mode);r.push({name:a,isDir:o})}return r})(i);break;case e.DELETE_DIR:p=(({path:e})=>(s.FS.rmdir(e),!0))(i);break;case e.MOUNT:p=(({fsType:e,options:t,mountPoint:r})=>{let a=e,o=s.FS.filesystems[a];return!!o&&(s.FS.mount(o,t,r),!0)})(i);break;case e.UNMOUNT:p=(({mountPoint:e})=>(s.FS.unmount(e),!0))(i);break;default:throw t}}catch(t){return void self.postMessage({id:n,type:e.ERROR,data:t.toString()})}p instanceof Uint8Array&&c.push(p.buffer),self.postMessage({id:n,type:E,data:p},c)}})(),{}})()));
|
||||
//# sourceMappingURL=814.ffmpeg.js.map
|
1
static/ffmpeg/814.ffmpeg.js.map
Normal file
21
static/ffmpeg/ffmpeg-core.js
Normal file
BIN
static/ffmpeg/ffmpeg-core.wasm
Executable file
1
static/ffmpeg/ffmpeg-util.js
Normal file
@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegUtil=t():e.FFmpegUtil=t()}(self,(()=>(()=>{"use strict";var e={591:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.HeaderContentLength=void 0,t.HeaderContentLength="Content-Length"},431:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ERROR_INCOMPLETED_DOWNLOAD=t.ERROR_RESPONSE_BODY_READER=void 0,t.ERROR_RESPONSE_BODY_READER=new Error("failed to get response body reader"),t.ERROR_INCOMPLETED_DOWNLOAD=new Error("failed to complete download")},915:function(e,t,o){var r=this&&this.__awaiter||function(e,t,o,r){return new(o||(o=Promise))((function(n,i){function d(e){try{l(r.next(e))}catch(e){i(e)}}function a(e){try{l(r.throw(e))}catch(e){i(e)}}function l(e){var t;e.done?n(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(d,a)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.toBlobURL=t.downloadWithProgress=t.importScript=t.fetchFile=void 0;const n=o(431),i=o(591);t.fetchFile=e=>r(void 0,void 0,void 0,(function*(){let t;if("string"==typeof e)t=/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?atob(e.split(",")[1]).split("").map((e=>e.charCodeAt(0))):yield(yield fetch(e)).arrayBuffer();else if(e instanceof URL)t=yield(yield fetch(e)).arrayBuffer();else{if(!(e instanceof File||e instanceof Blob))return new Uint8Array;t=yield(o=e,new Promise(((e,t)=>{const r=new FileReader;r.onload=()=>{const{result:t}=r;t instanceof ArrayBuffer?e(new Uint8Array(t)):e(new Uint8Array)},r.onerror=e=>{var o,r;t(Error(`File could not be read! Code=${(null===(r=null===(o=null==e?void 0:e.target)||void 0===o?void 0:o.error)||void 0===r?void 0:r.code)||-1}`))},r.readAsArrayBuffer(o)})))}var o;return new Uint8Array(t)})),t.importScript=e=>r(void 0,void 0,void 0,(function*(){return new Promise((t=>{const o=document.createElement("script"),r=()=>{o.removeEventListener("load",r),t()};o.src=e,o.type="text/javascript",o.addEventListener("load",r),document.getElementsByTagName("head")[0].appendChild(o)}))})),t.downloadWithProgress=(e,t)=>r(void 0,void 0,void 0,(function*(){var o;const r=yield fetch(e);let d;try{const a=parseInt(r.headers.get(i.HeaderContentLength)||"-1"),l=null===(o=r.body)||void 0===o?void 0:o.getReader();if(!l)throw n.ERROR_RESPONSE_BODY_READER;const c=[];let s=0;for(;;){const{done:o,value:r}=yield l.read(),i=r?r.length:0;if(o){if(-1!=a&&a!==s)throw n.ERROR_INCOMPLETED_DOWNLOAD;t&&t({url:e,total:a,received:s,delta:i,done:o});break}c.push(r),s+=i,t&&t({url:e,total:a,received:s,delta:i,done:o})}const f=new Uint8Array(s);let u=0;for(const e of c)f.set(e,u),u+=e.length;d=f.buffer}catch(o){console.log("failed to send download progress event: ",o),d=yield r.arrayBuffer(),t&&t({url:e,total:d.byteLength,received:d.byteLength,delta:0,done:!0})}return d})),t.toBlobURL=(e,o,n=!1,i)=>r(void 0,void 0,void 0,(function*(){const r=n?yield(0,t.downloadWithProgress)(e,i):yield(yield fetch(e)).arrayBuffer(),d=new Blob([r],{type:o});return URL.createObjectURL(d)}))}},t={};return function o(r){var n=t[r];if(void 0!==n)return n.exports;var i=t[r]={exports:{}};return e[r].call(i.exports,i,i.exports,o),i.exports}(915)})()));
|
2
static/ffmpeg/ffmpeg.js
Normal file
@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=(s={},{signal:r}={})=>(this.#e||(this.#e=new Worker(new URL(e.p+e.u(814),e.b),{type:void 0}),this.#o()),this.#i({type:t.LOAD,data:s},void 0,r));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}return s})()));
|
||||
//# sourceMappingURL=ffmpeg.js.map
|
1
static/ffmpeg/ffmpeg.js.map
Normal file
1
static/hls.min.js
vendored
BIN
static/logo.png
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 60 KiB |
33
static/logo.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 512 512"
|
||||
id="svg2"
|
||||
width="512"
|
||||
height="512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs id="defs2" />
|
||||
<rect width="512" height="512" fill="#4c082a" />
|
||||
<g
|
||||
transform="matrix(0.75272,0,0,0.75272,-1.1596187,-0.37987125)"
|
||||
id="g2">
|
||||
<circle
|
||||
fill="#1a1a1a"
|
||||
id="circle1"
|
||||
style="fill:#4c082a;fill-opacity:0"
|
||||
r="340.10001"
|
||||
cy="340.32001"
|
||||
cx="341.10999" />
|
||||
<path
|
||||
d="m 320.64,126.73 v 300.8 h 92.264 V 219.61 h 75.803 v -92.83 h -75.803 v -0.0508 z"
|
||||
fill="#f83240"
|
||||
id="path1"
|
||||
style="fill:#f83240;fill-opacity:1" />
|
||||
<path
|
||||
d="M 193.1,126.74 V 510.7 h 0.006 v 43.543 h 295.82 v -92.338 h -202.74 v -335.16 z"
|
||||
fill="#f83240"
|
||||
id="path2"
|
||||
style="fill:#f83240;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 943 B |
BIN
static/logo_full.png
Normal file
After Width: | Height: | Size: 219 KiB |
@ -1,77 +0,0 @@
|
||||
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
||||
(function () {
|
||||
if (Hls.isSupported()) {
|
||||
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
|
||||
videoSources.forEach(function (source) {
|
||||
var playlist = source.src;
|
||||
|
||||
var oldVideo = source.parentNode;
|
||||
var autoplay = oldVideo.classList.contains("hls_autoplay");
|
||||
|
||||
// If HLS is supported natively then don't use hls.js
|
||||
if (oldVideo.canPlayType(source.type)) {
|
||||
if (autoplay) {
|
||||
oldVideo.play();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace video with copy that will have all "source" elements removed
|
||||
var newVideo = oldVideo.cloneNode(true);
|
||||
var allSources = newVideo.querySelectorAll("source");
|
||||
allSources.forEach(function (source) {
|
||||
source.remove();
|
||||
});
|
||||
|
||||
// Empty source to enable play event
|
||||
newVideo.src = "about:blank";
|
||||
|
||||
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
|
||||
|
||||
function initializeHls() {
|
||||
newVideo.removeEventListener('play', initializeHls);
|
||||
|
||||
var hls = new Hls({ autoStartLoad: false });
|
||||
hls.loadSource(playlist);
|
||||
hls.attachMedia(newVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
hls.loadLevel = hls.levels.length - 1;
|
||||
hls.startLoad();
|
||||
newVideo.play();
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
var errorType = data.type;
|
||||
var errorFatal = data.fatal;
|
||||
if (errorFatal) {
|
||||
switch (errorType) {
|
||||
case Hls.ErrorType.NETWORK_ERROR:
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorType.MEDIA_ERROR:
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.error("HLS error", data);
|
||||
});
|
||||
}
|
||||
|
||||
newVideo.addEventListener('play', initializeHls);
|
||||
|
||||
if (autoplay) {
|
||||
newVideo.play();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var videos = document.querySelectorAll("video.hls_autoplay");
|
||||
videos.forEach(function (video) {
|
||||
video.setAttribute("autoplay", "");
|
||||
});
|
||||
}
|
||||
})();
|
||||
// @license-end
|
172
static/style.css
@ -112,6 +112,16 @@ pre, form, fieldset, table, th, td, select, input {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
html.fixed_navbar {
|
||||
scroll-padding-top: 50px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
html.fixed_navbar {
|
||||
scroll-padding-top: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
padding-bottom: var(--footer-height);
|
||||
@ -177,6 +187,11 @@ nav #redlib {
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings_link {
|
||||
opacity: 0.8;
|
||||
margin-left: 10px;
|
||||
@ -969,10 +984,6 @@ a.search_subreddit:hover {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.gallery figcaption {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.gallery .outbound_url {
|
||||
color: var(--accent);
|
||||
text-overflow: ellipsis;
|
||||
@ -998,6 +1009,13 @@ a.search_subreddit:hover {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.post_body img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.post_poll {
|
||||
grid-area: post_poll;
|
||||
padding: 5px 15px 5px 12px;
|
||||
@ -1084,7 +1102,7 @@ a.search_subreddit:hover {
|
||||
display: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
@media screen and (min-width: 481px) {
|
||||
#post_links > li.mobile_item {
|
||||
display: none;
|
||||
}
|
||||
@ -1161,6 +1179,22 @@ a.search_subreddit:hover {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.comment img {
|
||||
max-width: 50%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.comment img {
|
||||
max-width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.comment figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment_left, .comment_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1291,6 +1325,83 @@ summary.comment_data {
|
||||
|
||||
/* Layouts */
|
||||
|
||||
.old .post {
|
||||
border-radius: 5px;
|
||||
background: var(--post);
|
||||
box-shadow: var(--shadow);
|
||||
display: grid;
|
||||
transition: 0.2s background;
|
||||
grid-template: "post_score post_thumbnail post_header" auto
|
||||
"post_score post_thumbnail post_title" 1fr
|
||||
"post_score post_thumbnail post_media" auto
|
||||
"post_score post_thumbnail post_body" auto
|
||||
"post_score post_thumbnail post_poll" auto
|
||||
"post_score post_thumbnail post_notification" auto
|
||||
"post_score post_thumbnail post_footer" auto
|
||||
/ 4.5em minmax(0, 140px) fit-content(min(90%, 90%))
|
||||
}
|
||||
|
||||
/* .old .post_thumbnail {
|
||||
margin-left: 20px;
|
||||
} */
|
||||
|
||||
.old .post_score {
|
||||
background-color: #ff93da08;
|
||||
border-radius: 8px;
|
||||
align-self: center;
|
||||
width: 53px;
|
||||
padding-left: 5px; /* REMINDER:pls compact testing only */
|
||||
border-top-width: 0px;
|
||||
border-top-style: solid;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-right: 5px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.old .post_media_content {
|
||||
max-width: calc(65% - 40px);
|
||||
}
|
||||
|
||||
.old .highlighted > .post_score {
|
||||
background-color: unset;
|
||||
align-self: unset;
|
||||
margin-top: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.old .post_score .label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-anchor: middle;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.old .post_preview {
|
||||
max-height: 72px;
|
||||
}
|
||||
|
||||
.old #column_one {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.old #commentQueryForms {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.old .commentQuery {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
|
||||
.old main {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.compact .post:not(.highlighted) {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
@ -1406,6 +1517,7 @@ summary.comment_data {
|
||||
box-shadow: var(--shadow);
|
||||
margin-left: 20px;
|
||||
background: var(--foreground);
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
aside.prefs {
|
||||
@ -1458,10 +1570,19 @@ input[type="submit"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md > *:not(:first-child) {
|
||||
.md > p:not(:first-child):not(:last-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.md > figure:first-of-type {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.md > figure:not(:first-of-type) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.md h1 { font-size: 22px; }
|
||||
.md h2 { font-size: 20px; }
|
||||
.md h3 { font-size: 18px; }
|
||||
@ -1714,3 +1835,42 @@ td, th {
|
||||
justify-content: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.video-options {
|
||||
border: 2px var(--outside) solid;
|
||||
margin-top: 8px;
|
||||
float: right;
|
||||
border-radius: 5px;
|
||||
height: 35px;
|
||||
height: 35px;
|
||||
margin: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.video-options option {
|
||||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.video-options option:hover {
|
||||
background-color: var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.download {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
color: var(--accent);
|
||||
background-color: var(--outside);
|
||||
}
|
||||
|
||||
.download:hover {
|
||||
background-color: var(--foreground);
|
||||
/*color: var(--);*/
|
||||
}
|
||||
|
||||
.download:active {
|
||||
background-color: var(--background);
|
||||
}
|
17
static/themes/catppuccin.css
Normal file
@ -0,0 +1,17 @@
|
||||
/* Catppuccin theme setting */
|
||||
.catppuccin {
|
||||
--accent: #b4befe; /* lavender */
|
||||
--green: #a6e3a1; /* green */
|
||||
--text: #cdd6f4; /* text */
|
||||
--foreground: #181825; /* mantle */
|
||||
--background: #1e1e2e; /* base */
|
||||
--outside: #11111b; /* crust */
|
||||
--post: #11111b; /* crust */
|
||||
--panel-border: none;
|
||||
--highlighted: #313244; /* surface0 */
|
||||
--visited: #6c7086; /* overlay0 */
|
||||
--shadow: 0 0 0 transparent;
|
||||
|
||||
--nsfw: #fab387; /* peach */
|
||||
--admin: #eba0ac; /* maroon */
|
||||
}
|
14
static/themes/icebergDark.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* icebergDark theme setting */
|
||||
.icebergDark {
|
||||
--accent: #85a0c7;
|
||||
--green: #b5bf82;
|
||||
--text: #c6c8d1;
|
||||
--foreground: #454d73;
|
||||
--background: #161821;
|
||||
--outside: #1f2233;
|
||||
--post: #1f2233;
|
||||
--panel-border: 1px solid #454d73;
|
||||
--highlighted: #0f1117;
|
||||
--visited: #0f1117;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
228
static/videoUtils.js
Normal file
@ -0,0 +1,228 @@
|
||||
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
||||
let ffmpeg = null;
|
||||
(function () {
|
||||
if (Hls.isSupported()) {
|
||||
|
||||
var downloadsEnabled = document.cookie.split("; ").find((row) => row.startsWith("ffmpeg_video_downloads="))?.split("=")[1] == "on";
|
||||
|
||||
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
|
||||
videoSources.forEach(function (source) {
|
||||
var playlist = source.src;
|
||||
|
||||
var oldVideo = source.parentNode;
|
||||
var autoplay = oldVideo.classList.contains("hls_autoplay");
|
||||
|
||||
// If HLS is supported natively then don't use hls.js
|
||||
if (oldVideo.canPlayType(source.type) === "probably") {
|
||||
if (autoplay) {
|
||||
oldVideo.play();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace video with copy that will have all "source" elements removed
|
||||
var newVideo = oldVideo.cloneNode(true);
|
||||
var allSources = newVideo.querySelectorAll("source");
|
||||
allSources.forEach(function (source) {
|
||||
source.remove();
|
||||
});
|
||||
|
||||
// Empty source to enable play event
|
||||
newVideo.src = "about:blank";
|
||||
|
||||
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
|
||||
|
||||
function initializeHls() {
|
||||
newVideo.removeEventListener('play', initializeHls);
|
||||
var hls = new Hls({ autoStartLoad: false });
|
||||
hls.loadSource(playlist);
|
||||
hls.attachMedia(newVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
hls.loadLevel = hls.levels.length - 1;
|
||||
var availableLevels = hls.levels.map(function(level) {
|
||||
return {
|
||||
height: level.height,
|
||||
width: level.width,
|
||||
bitrate: level.bitrate,
|
||||
};
|
||||
});
|
||||
|
||||
addQualitySelector(newVideo, hls, availableLevels);
|
||||
if (downloadsEnabled){ addVideoDownload(newVideo, hls); }
|
||||
hls.startLoad();
|
||||
newVideo.play();
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
var errorType = data.type;
|
||||
var errorFatal = data.fatal;
|
||||
if (errorFatal) {
|
||||
switch (errorType) {
|
||||
case Hls.ErrorType.NETWORK_ERROR:
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorType.MEDIA_ERROR:
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.error("HLS error", data);
|
||||
});
|
||||
}
|
||||
|
||||
if (downloadsEnabled){
|
||||
const { fetchFile } = FFmpegUtil;
|
||||
const { FFmpeg } = FFmpegWASM;
|
||||
|
||||
function addVideoDownload(videoElement, hlsInstance) {
|
||||
var mediaStream = [];
|
||||
var downloadButton = document.createElement('button');
|
||||
downloadButton.classList.add('video-options','download');
|
||||
downloadButton.innerText = "⏳"
|
||||
const mergeStreams = async () => {
|
||||
if (ffmpeg === null) {
|
||||
ffmpeg = new FFmpeg();
|
||||
await ffmpeg.load({
|
||||
coreURL: "/ffmpeg/ffmpeg-core.js",
|
||||
});
|
||||
ffmpeg.on("log", ({ message }) => {
|
||||
console.log(message); // This is quite noisy but i will include it
|
||||
})
|
||||
ffmpeg.on("progress", ({ progress, time }) => { // Progress TODO: show progress ring around button not just ⏳
|
||||
// console.log("ffmpeg prog:",progress * 100)
|
||||
});
|
||||
}
|
||||
// Combine Video Audio Streams
|
||||
await ffmpeg.writeFile("video", await fetchFile(concatBlob(mediaStream['video'])));
|
||||
await ffmpeg.writeFile("audio", await fetchFile(concatBlob(mediaStream['audio'])));
|
||||
console.time('ffmpeg-exec');
|
||||
await ffmpeg.exec(['-i', "video", '-i', "audio",'-c:v', "copy", '-c:a', "aac", 'output.mp4']);
|
||||
console.timeEnd('ffmpeg-exec')
|
||||
|
||||
// Save
|
||||
const toSlug = (str) => {
|
||||
return str
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[\W_]+/g, '-')
|
||||
.toLowerCase()
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
|
||||
const data = await ffmpeg.readFile('output.mp4');
|
||||
saveAs(new Blob([data.buffer]),filename);
|
||||
return
|
||||
}
|
||||
function saveAs(blob, filename) { // Yeah ok...
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
a.style = "display: none";
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
function concatBlob(inputArray) {
|
||||
var totalLength = inputArray.reduce(function (prev, cur) {
|
||||
return prev + cur.length
|
||||
}, 0);
|
||||
var result = new Uint8Array(totalLength);
|
||||
var offset = 0;
|
||||
inputArray.forEach(function (element) {
|
||||
result.set(element, offset);
|
||||
offset += element.length;
|
||||
});
|
||||
return new Blob([result], {
|
||||
type: 'application/octet-stream'
|
||||
});
|
||||
}
|
||||
function getStreams() {
|
||||
var video = document.createElement('video');
|
||||
video.autoplay = true;
|
||||
var dataStreams = {
|
||||
'video': [],
|
||||
'audio': []
|
||||
};
|
||||
mediaStream = dataStreams; // Update stream
|
||||
|
||||
hlsInstance.on(Hls.Events.BUFFER_APPENDING, function (event, data) {
|
||||
dataStreams[data.type].push(data.data);
|
||||
});
|
||||
var isDownloading = false
|
||||
function startDownload() {
|
||||
if (!isDownloading) { isDownloading = true } else { return }
|
||||
downloadButton.innerText = "⏳"
|
||||
mergeStreams()
|
||||
.then(_ => {
|
||||
isDownloading = false
|
||||
downloadButton.innerText = "⭳"
|
||||
});
|
||||
}
|
||||
|
||||
function waitForLoad() {
|
||||
const poll = resolve => {
|
||||
if(hlsInstance._media.buffered.length === 1 &&
|
||||
hlsInstance._media.buffered.start(0) === 0 &&
|
||||
hlsInstance._media.buffered.end(0) === hlsInstance._media.duration)
|
||||
resolve();
|
||||
else setTimeout(_ => poll(resolve), 400);
|
||||
}
|
||||
return new Promise(poll);
|
||||
}
|
||||
|
||||
waitForLoad(_ => flag === true)
|
||||
.then(_ => {
|
||||
downloadButton.innerText = "⭳"
|
||||
downloadButton.addEventListener('click', startDownload);
|
||||
});
|
||||
}
|
||||
|
||||
videoElement.parentNode.appendChild(downloadButton);
|
||||
getStreams()
|
||||
}
|
||||
}
|
||||
|
||||
function addQualitySelector(videoElement, hlsInstance, availableLevels) {
|
||||
var qualitySelector = document.createElement('select');
|
||||
qualitySelector.classList.add('video-options');
|
||||
var last = availableLevels.length - 1;
|
||||
availableLevels.forEach(function (level, index) {
|
||||
var option = document.createElement('option');
|
||||
option.value = index.toString();
|
||||
var bitrate = (level.bitrate / 1_000).toFixed(0);
|
||||
option.text = level.height + 'p (' + bitrate + ' kbps)';
|
||||
if (index === last) {
|
||||
option.selected = "selected";
|
||||
}
|
||||
qualitySelector.appendChild(option);
|
||||
});
|
||||
qualitySelector.selectedIndex = availableLevels.length - 1;
|
||||
qualitySelector.addEventListener('change', function () {
|
||||
var selectedIndex = qualitySelector.selectedIndex;
|
||||
hlsInstance.nextLevel = selectedIndex;
|
||||
hlsInstance.startLoad();
|
||||
});
|
||||
|
||||
videoElement.parentNode.appendChild(qualitySelector);
|
||||
}
|
||||
|
||||
newVideo.addEventListener('play', initializeHls);
|
||||
|
||||
if (autoplay) {
|
||||
newVideo.play();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var videos = document.querySelectorAll("video.hls_autoplay");
|
||||
videos.forEach(function (video) {
|
||||
video.setAttribute("autoplay", "");
|
||||
});
|
||||
}
|
||||
})();
|
||||
// @license-end
|
@ -1,7 +1,7 @@
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if prefs.fixed_navbar == "on" %}fixed_navbar{% endif %}">
|
||||
<head>
|
||||
{% block head %}
|
||||
<title>{% block title %}Redlib{% endblock %}</title>
|
||||
@ -28,14 +28,14 @@
|
||||
</head>
|
||||
<body class="
|
||||
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
|
||||
{% if prefs.wide == "on" %} wide{% endif %}
|
||||
{% if prefs.wide == "on" && prefs.layout != "old" %} wide{% endif %}
|
||||
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
|
||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||
<!-- NAVIGATION BAR -->
|
||||
<nav class="
|
||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||
<div id="logo">
|
||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">lib.</span></a>
|
||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib</span></a>
|
||||
{% block subscriptions %}{% endblock %}
|
||||
</div>
|
||||
{% block search %}{% endblock %}
|
||||
@ -76,7 +76,7 @@
|
||||
<a href="/info" title="View instance information">ⓘ View instance info</a>
|
||||
</div>
|
||||
<div class="footer-button">
|
||||
<a href="https://github.com/redlib-org/redlib" title="View code on GitHub"><> Code</a>
|
||||
<a href="https://git.stardust.wtf/iridium/redsunlib" title="View code on git.stardust.wtf"><> Code</a>
|
||||
</div>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
@ -97,9 +97,13 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if params.typed != "sr_user" %}
|
||||
|
@ -31,13 +31,14 @@
|
||||
<div class="prefs-group">
|
||||
<label for="layout">Layout:</label>
|
||||
<select name="layout" id="layout">
|
||||
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
|
||||
{% call utils::options(prefs.layout, ["card", "clean", "compact","old"], "card") %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="wide">Wide UI:</label>
|
||||
<input type="hidden" value="off" name="wide">
|
||||
<input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" %}checked{% endif %}>
|
||||
{% if prefs.layout == "old" %}<span>ⓘ Wide UI is always enabled with this layout</span>{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@ -71,19 +72,36 @@
|
||||
<input type="hidden" value="off" name="autoplay_videos">
|
||||
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<div class="prefs-group">
|
||||
<label for="fixed_navbar">Keep navbar fixed</label>
|
||||
<input type="hidden" value="off" name="fixed_navbar">
|
||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<input type="hidden" value="off" name="fixed_navbar">
|
||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
|
||||
<input type="hidden" value="off" name="hide_sidebar_and_summary">
|
||||
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="use_hls">Use HLS for videos</label>
|
||||
{% if prefs.ffmpeg_video_downloads != "on" %}
|
||||
<details id="feeds">
|
||||
<summary>Why?</summary>
|
||||
<div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}<u>ⓘ HLS is required for downloads</u>{% endif %}
|
||||
<input type="hidden" value="off" name="use_hls">
|
||||
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
|
||||
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.ffmpeg_video_downloads == "on" %}disabled{% endif %} {% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="ffmpeg_video_downloads">Use FFmpeg to download videos</label>
|
||||
<details id="feeds">
|
||||
<summary>Why?</summary>
|
||||
<div id="feed_list" class="helper">Downloading videos with audio requires ffmpeg (via ffmpeg.wasm) to be enabled to combine video and audio tracks. Therefore, this toggle lets you either use Redlib WebAssembly-free or utilize this feature. (videos will still play with audio)</div>
|
||||
</details>
|
||||
<input type="hidden" value="off" name="ffmpeg_video_downloads">
|
||||
<input type="checkbox" name="ffmpeg_video_downloads" id="ffmpeg_video_downloads" {% if prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
|
||||
@ -142,7 +160,7 @@
|
||||
|
||||
<div id="settings_note">
|
||||
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -64,25 +64,29 @@
|
||||
{% call utils::post_in_list(post) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if prefs.use_hls == "on" %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<footer>
|
||||
{% if !ends.0.is_empty() %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">< PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if !ends.1.is_empty() %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT ></a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %}
|
||||
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) && prefs.hide_sidebar_and_summary != "on" %}
|
||||
<aside>
|
||||
{% if is_filtered %}
|
||||
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
||||
@ -133,7 +137,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="panel" id="sidebar">
|
||||
<details class="panel" id="sidebar" open>
|
||||
<summary id="sidebar_label">Sidebar</summary>
|
||||
<div id="sidebar_contents">
|
||||
{{ sub.info|safe }}
|
||||
|
@ -71,9 +71,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if prefs.use_hls == "on" %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -117,7 +117,11 @@
|
||||
</a>
|
||||
</div>
|
||||
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
|
||||
@ -125,7 +129,7 @@
|
||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% else %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
|
||||
@ -250,7 +254,7 @@
|
||||
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
</div>
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||
|