├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── instance.yml └── workflows │ ├── docker.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── build.rs ├── docker-compose.yml ├── instances.json ├── src ├── album.rs ├── api.rs ├── artist.rs ├── errors.rs ├── genius.rs ├── home.rs ├── lyrics.rs ├── main.rs ├── resource.rs ├── search.rs ├── settings.rs ├── templates.rs └── utils.rs ├── static ├── font │ ├── InterVariable.woff2 │ ├── LICENSE.txt │ └── inter.css ├── icon │ ├── facebook.svg │ ├── instagram.svg │ └── twitter.svg ├── manifest.json └── style │ ├── artist.css │ ├── error.css │ ├── home.css │ ├── lyrics.css │ ├── search.css │ ├── settings.css │ ├── song.css │ ├── style.css │ └── theme │ ├── catppuccin-frappe.css │ ├── catppuccin-latte.css │ ├── catppuccin-macchiato.css │ ├── catppuccin-mocha.css │ ├── github-dark.css │ ├── github-light.css │ ├── gruvbox-dark.css │ ├── gruvbox-light.css │ ├── sweet.css │ └── themes.json └── templates ├── 400.html ├── 404.html ├── 500.html ├── album.html ├── artist.html ├── base.html ├── home.html ├── lyrics.html ├── search.html ├── settings.html └── song.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [{*.yml,*.json}] 11 | indent_size = 2 12 | 13 | [Cargo.lock] 14 | end_of_line = unset 15 | insert_final_newline = unset 16 | charset = unset 17 | indent_style = unset 18 | indent_size = unset 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | # Force Batch files to CRLF 3 | *.bat eol=crlf -text 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug with Intellectual. 3 | labels: [ "bug" ] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Is there an existing issue for this? 8 | description: Please search to see if an issue already exists for the bug you encountered. 9 | options: 10 | - label: I have searched the existing issues 11 | required: true 12 | - type: checkboxes 13 | attributes: 14 | label: Are you using the latest version of Intellectual? 15 | description: Please make sure you're using the latest version of Intellectual as it's possible your issue has already been fixed. 16 | options: 17 | - label: I am using the latest version of Intellectual. 18 | required: true 19 | - type: textarea 20 | id: bug-description 21 | attributes: 22 | label: Describe the bug 23 | description: Describe the bug in as much detail as you can. 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Anything else? 29 | description: | 30 | Links? References? Anything that will give us more context about the issue you are encountering! 31 | validations: 32 | required: false 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Discussion Board 5 | url: https://github.com/Insprill/intellectual/discussions 6 | about: Have a question? Look through the existing discussions, or create a new one! 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature for Intellectual. 3 | labels: [ "enhancement" ] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Is there an existing issue for this? 8 | description: Please search to see if an issue already exists for the feature you want. 9 | options: 10 | - label: I have searched the existing issues 11 | required: true 12 | - type: textarea 13 | id: feature-description 14 | attributes: 15 | label: Describe the feature 16 | description: Describe the feature in as much detail as you can. 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/instance.yml: -------------------------------------------------------------------------------- 1 | name: New Instance 2 | description: Request your own instance be added to the instances list 3 | labels: [ "instance" ] 4 | body: 5 | - type: input 6 | id: url 7 | attributes: 8 | label: URL 9 | description: The URL linking to your Intellectual instance. 10 | placeholder: ex. https://intellectual.insprill.net 11 | validations: 12 | required: true 13 | - type: input 14 | id: region 15 | attributes: 16 | label: Region 17 | description: The region the instance is hosted in. 18 | placeholder: US 19 | validations: 20 | required: true 21 | - type: checkboxes 22 | id: cloudflare 23 | attributes: 24 | label: Cloudflare 25 | description: Is the instance proxied behind Cloudflare? Using Cloudflare as DNS only does not apply. 26 | options: 27 | - label: Is the instance proxied behind Cloudflare? 28 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | DOCKER_IMAGE: insprill/intellectual 7 | DOCKER_PLATFORMS: linux/amd64,linux/arm64 8 | 9 | jobs: 10 | build-docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set Docker push status 17 | run: echo "DOCKER_PUSH=${{ !env.ACT && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.event_name != 'pull_request' }}" >> $GITHUB_ENV 18 | 19 | - name: Setup act 20 | run: "sudo chown runner:docker /var/run/docker.sock" 21 | if: ${{ env.ACT }} 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v3 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v3 28 | 29 | - name: Login to DockerHub 30 | uses: docker/login-action@v3 31 | if: ${{ env.DOCKER_PUSH == 'true' }} 32 | with: 33 | username: ${{ secrets.DOCKERHUB_USERNAME }} 34 | password: ${{ secrets.DOCKERHUB_TOKEN }} 35 | 36 | - name: Docker Hub Description 37 | uses: peter-evans/dockerhub-description@v3 38 | if: ${{ env.DOCKER_PUSH == 'true' }} 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USERNAME }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | repository: ${{ env.DOCKER_IMAGE }} 43 | enable-url-completion: true 44 | 45 | - name: Set Environment Variables 46 | run: | 47 | echo "IN_IS_GIT=true" >> .env 48 | echo "IN_GIT_HASH=$(git rev-parse --short HEAD)" >> .env 49 | echo "IN_GIT_DIRTY=$([[ -n $(git status --porcelain) ]] && echo true || echo false)" >> .env 50 | echo "IN_GIT_TAG=$(git describe --tags --exact-match)" >> .env 51 | echo "IN_GIT_TAGGED=$([[ -n $(git describe --tags --exact-match) ]] && echo true || echo false)" >> .env 52 | echo "IN_GIT_REMOTE_URL=$(git remote get-url origin)" >> .env 53 | 54 | - name: Build & Push 55 | uses: docker/build-push-action@v5 56 | with: 57 | context: . # Required for running with act 58 | push: ${{ env.DOCKER_PUSH == 'true' }} 59 | tags: ${{ env.DOCKER_IMAGE }}:${{ startsWith(github.ref, 'refs/tags/v') && 'latest' || 'develop' }} 60 | platforms: ${{ env.DOCKER_PLATFORMS }} 61 | cache-from: type=gha 62 | cache-to: type=gha,mode=max 63 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }}-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | # x86_64 16 | - target: 'x86_64-pc-windows-msvc' 17 | os: windows 18 | - target: 'x86_64-unknown-linux-gnu' 19 | os: ubuntu 20 | - target: 'x86_64-unknown-linux-musl' 21 | os: ubuntu 22 | # aarch64 (armv8) 23 | - target: 'aarch64-unknown-linux-gnu' 24 | os: ubuntu 25 | - target: 'aarch64-unknown-linux-musl' 26 | os: ubuntu 27 | # armv7 28 | - target: 'armv7-unknown-linux-gnueabihf' 29 | os: ubuntu 30 | - target: 'armv7-unknown-linux-musleabihf' 31 | os: ubuntu 32 | # armv6 33 | - target: 'arm-unknown-linux-gnueabihf' 34 | os: ubuntu 35 | - target: 'arm-unknown-linux-musleabihf' 36 | os: ubuntu 37 | steps: 38 | - name: Checkout Repository 39 | uses: actions/checkout@v4 40 | 41 | - name: Install Cross 42 | run: cargo install --git https://github.com/cross-rs/cross cross 43 | 44 | - name: Setup Cache 45 | uses: Swatinem/rust-cache@v2 46 | 47 | # cross doesn't support msvc toolchains out-of-the-box (https://github.com/cross-rs/cross-toolchains) 48 | - name: Build (Windows) 49 | if: matrix.os == 'windows' 50 | run: cargo build --release --target=${{ matrix.target }} 51 | 52 | - name: Build (Linux) 53 | if: matrix.os == 'ubuntu' 54 | run: cross build --release --target=${{ matrix.target }} 55 | 56 | - name: Upload Artifact 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: Intellectual-${{ matrix.target }} 60 | if-no-files-found: ignore 61 | path: | 62 | target/${{ matrix.target }}/release/intellectual 63 | target/${{ matrix.target }}/release/intellectual.exe 64 | 65 | code-style: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Checkout Repository 69 | uses: actions/checkout@v4 70 | 71 | - name: Setup Cache 72 | uses: Swatinem/rust-cache@v2 73 | 74 | - name: Check rustfmt 75 | run: cargo fmt --all -- --check 76 | 77 | - name: Check Clippy 78 | run: cargo clippy -- -D warnings 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudioCode template 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | *.code-workspace 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | ### Rust template 13 | # Generated by Cargo 14 | # will have compiled files and executables 15 | debug/ 16 | target/ 17 | 18 | # These are backup files generated by rustfmt 19 | **/*.rs.bk 20 | 21 | ### JetBrains template 22 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 23 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 24 | 25 | # User-specific stuff 26 | .idea/ 27 | *.iml 28 | *.ipr 29 | 30 | # CMake 31 | cmake-build-*/ 32 | 33 | # File-based project format 34 | *.iws 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | 51 | ### Linux template 52 | *~ 53 | 54 | # temporary files which can be created if a process still has a handle open of a deleted file 55 | .fuse_hidden* 56 | 57 | # KDE directory preferences 58 | .directory 59 | 60 | # Linux trash folder which might appear on any partition or disk 61 | .Trash-* 62 | 63 | # .nfs files are created when an open file is removed but is still being accessed 64 | .nfs* 65 | 66 | ### Windows template 67 | # Windows thumbnail cache files 68 | Thumbs.db 69 | Thumbs.db:encryptable 70 | ehthumbs.db 71 | ehthumbs_vista.db 72 | 73 | # Dump file 74 | *.stackdump 75 | 76 | # Folder config file 77 | [Dd]esktop.ini 78 | 79 | # Recycle Bin used on file shares 80 | $RECYCLE.BIN/ 81 | 82 | # Windows Installer files 83 | *.cab 84 | *.msi 85 | *.msix 86 | *.msm 87 | *.msp 88 | 89 | # Windows shortcuts 90 | *.lnk 91 | 92 | ### macOS template 93 | # General 94 | .DS_Store 95 | .AppleDouble 96 | .LSOverride 97 | 98 | # Thumbnails 99 | ._* 100 | 101 | # Files that might appear in the root of a volume 102 | .DocumentRevisions-V100 103 | .fseventsd 104 | .Spotlight-V100 105 | .TemporaryItems 106 | .Trashes 107 | .VolumeIcon.icns 108 | .com.apple.timemachine.donotpresent 109 | 110 | # Directories potentially created on remote AFP share 111 | .AppleDB 112 | .AppleDesktop 113 | Network Trash Folder 114 | Temporary Items 115 | .apdisk 116 | 117 | ### Act 118 | # Local GitHub secrets 119 | *.secrets 120 | 121 | ### Misc 122 | # Local env vars 123 | *.env 124 | # Cert files 125 | *.pem 126 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## [0.8.2] - 2025-05-23 10 | 11 | ### Fixed 12 | - Song summary and contributor information being present at the beginning of all lyrics. 13 | - `manifest.json` being unretreivable. 14 | - All error pages returning a `200` status code. 15 | 16 | ## [0.8.1] - 2024-04-29 17 | 18 | ### Fixed 19 | - The 'View on Genius' button not working for lyrics. 20 | - Being able to submit an empty search, leading to a blank results page. 21 | - The seach input not being properly marked as such. 22 | 23 | 24 | ## [0.8.0] - 2024-04-19 25 | 26 | ### Changed 27 | - All paths have been updated to match Genius to make redirecting easier. If you have any redirects, you'll need to update them to only modify the host. 28 | - Clicking on the version in the navbar will now take you to the exact commit/tag the version was built from. 29 | 30 | 31 | ## [0.7.0] - 2024-02-16 32 | 33 | ### Added 34 | - Support for aarch64. 35 | - Support for Brotli compression. 36 | - A new docker tag for development versions (`develop`). 37 | 38 | ### Fixed 39 | - Album cards in artist descriptions not rendering correctly ([#23] by [@ButteredCats]). 40 | - Images in artist descriptions not being proxied, causing them not to load. 41 | - Artist/album/lyric links in artist descriptions pointing to Genius. 42 | - Artist link in albums directing to an invalid page. 43 | - Proxied images not sending a Content-Type header. 44 | - Static assets not being stored in shared caches (e.g. Cloudflare). 45 | - Pages not being stored in local (browser) caches. 46 | - Version link in navbar not working. 47 | 48 | ### Changed 49 | - Images are now optimized before being proxied (70-80% smaller). 50 | - The Git hash is now included in versions that aren't built against a release tag. 51 | 52 | 53 | ## [0.6.0] - 2023-10-25 54 | 55 | ### Added 56 | - A settings menu to change theme. 57 | - Pre-built binary for musl libc. 58 | 59 | ### Fixed 60 | - Long titles covering other UI elements ([#15] by [@Ftonans]). 61 | 62 | ### Changed 63 | - Reduced binary size by 20%. 64 | 65 | ### Removed 66 | - Pre-built macOS binary. 67 | 68 | 69 | ## [0.5.1] - 2023-07-07 70 | 71 | ### Fixed 72 | - Lyrics not showing for songs with no verse headers. 73 | 74 | 75 | ## [0.5.0] - 2023-06-01 76 | 77 | ### Added 78 | - The ability to go to artists/albums via path. 79 | 80 | ### Removed 81 | - The ability to go to artists/albums via ID. 82 | 83 | 84 | ## [0.4.0] - 2023-05-18 85 | 86 | ### Added 87 | - Support for viewing albums. 88 | - Support for TLS connections. 89 | - Flag for the desired Keep-Alive timeout. The default has been increased to 15 seconds from 5. 90 | - Hover animations to pagination buttons. 91 | 92 | ### Fixed 93 | - The lyric page's "View on Genius" button taking the user to an invalid URL. 94 | 95 | ### Removed 96 | - The requirement for a Genius API token. 97 | 98 | 99 | ## [0.3.1] - 2023-05-09 100 | 101 | ### Fixed 102 | - The artist image taking up the entire background on small Safari viewports. 103 | - The image URL on the artist page not being URL encoded. 104 | 105 | 106 | ## [0.3.0] - 2023-05-09 107 | 108 | ### Added 109 | - A section for the top 5 songs on the artist page. 110 | - More information to page titles. 111 | - Error pages for 400, 404, and 500. 112 | - Cache-Control headers to static resources. 113 | - A description meta tag to the home page. 114 | - Logging for internal server errors. 115 | 116 | ### Fixed 117 | - Font scaling on smaller devices. 118 | - Browsers not invalidating static assets between versions. 119 | - Multiple panics from invalid requests/responses. 120 | - The logo being hard to see in light mode. 121 | - The lyric parser sometimes creating empty lines. 122 | - The lyric parser creating new lines where annotations start/end. 123 | 124 | ### Changed 125 | - The default address to `0.0.0.0`. 126 | 127 | 128 | ## [0.2.0] - 2022-11-20 129 | 130 | ### Added 131 | - Paginated searches. 132 | - Light/dark themes. 133 | - An artist page to view information about an artist. 134 | - A 'Search on Genius' button. 135 | - More hover effects. 136 | - Gzip compression for responses. 137 | - A flag to set worker count. 138 | - Security response headers. 139 | - A web app manifest. 140 | 141 | ### Fixed 142 | - Static files being served from disk instead of being bundled. 143 | - Page view counts being posted in a separate batch. 144 | - The genius response being relayed if an image couldn't be fetched. 145 | 146 | ### Changed 147 | - Improved responsiveness on the lyrics page ([#6] by [@SeniorCluckers]). 148 | - Updated fallback font to not be as jarring from Inter. 149 | - Updated dependencies. 150 | 151 | 152 | ## [0.1.0] - 2022-10-13 153 | 154 | - Initial release. 155 | 156 | 157 | 158 | [@ButteredCats]: https://github.com/ButteredCats 159 | [@Ftonans]: https://github.com/Ftonans 160 | [@SeniorCluckers]: https://github.com/SeniorCluckers 161 | 162 | 163 | [#23]: https://github.com/Insprill/intellectual/pull/23 164 | [#15]: https://github.com/Insprill/intellectual/pull/15 165 | [#6]: https://github.com/Insprill/intellectual/pull/6 166 | 167 | 168 | [Unreleased]: https://github.com/Insprill/intellectual/compare/v0.8.2...HEAD 169 | [0.8.2]: https://github.com/Insprill/intellectual/compare/v0.8.1...v0.8.2 170 | [0.8.1]: https://github.com/Insprill/intellectual/compare/v0.8.0...v0.8.1 171 | [0.8.0]: https://github.com/Insprill/intellectual/compare/v0.7.0...v0.8.0 172 | [0.7.0]: https://github.com/Insprill/intellectual/compare/v0.6.0...v0.7.0 173 | [0.6.0]: https://github.com/Insprill/intellectual/compare/v0.5.1...v0.6.0 174 | [0.5.1]: https://github.com/Insprill/intellectual/compare/v0.5.0...v0.5.1 175 | [0.5.0]: https://github.com/Insprill/intellectual/compare/v0.4.0...v0.5.0 176 | [0.4.0]: https://github.com/Insprill/intellectual/compare/v0.3.1...v0.4.0 177 | [0.3.1]: https://github.com/Insprill/intellectual/compare/v0.3.0...v0.3.1 178 | [0.3.0]: https://github.com/Insprill/intellectual/compare/v0.2.0...v0.3.0 179 | [0.2.0]: https://github.com/Insprill/intellectual/compare/v0.1.0...v0.2.0 180 | [0.1.0]: https://github.com/Insprill/intellectual/releases/tag/v0.1.0 181 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "actix-codec" 7 | version = "0.5.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" 10 | dependencies = [ 11 | "bitflags 2.9.1", 12 | "bytes", 13 | "futures-core", 14 | "futures-sink", 15 | "memchr", 16 | "pin-project-lite", 17 | "tokio", 18 | "tokio-util", 19 | "tracing", 20 | ] 21 | 22 | [[package]] 23 | name = "actix-http" 24 | version = "3.11.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" 27 | dependencies = [ 28 | "actix-codec", 29 | "actix-rt", 30 | "actix-service", 31 | "actix-tls", 32 | "actix-utils", 33 | "base64 0.22.1", 34 | "bitflags 2.9.1", 35 | "brotli", 36 | "bytes", 37 | "bytestring", 38 | "derive_more 2.0.1", 39 | "encoding_rs", 40 | "flate2", 41 | "foldhash", 42 | "futures-core", 43 | "h2", 44 | "http 0.2.12", 45 | "httparse", 46 | "httpdate", 47 | "itoa", 48 | "language-tags", 49 | "local-channel", 50 | "mime", 51 | "percent-encoding", 52 | "pin-project-lite", 53 | "rand 0.9.1", 54 | "sha1", 55 | "smallvec", 56 | "tokio", 57 | "tokio-util", 58 | "tracing", 59 | ] 60 | 61 | [[package]] 62 | name = "actix-macros" 63 | version = "0.2.4" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 66 | dependencies = [ 67 | "quote", 68 | "syn", 69 | ] 70 | 71 | [[package]] 72 | name = "actix-router" 73 | version = "0.5.3" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" 76 | dependencies = [ 77 | "bytestring", 78 | "cfg-if", 79 | "http 0.2.12", 80 | "regex-lite", 81 | "serde", 82 | "tracing", 83 | ] 84 | 85 | [[package]] 86 | name = "actix-rt" 87 | version = "2.10.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" 90 | dependencies = [ 91 | "futures-core", 92 | "tokio", 93 | ] 94 | 95 | [[package]] 96 | name = "actix-server" 97 | version = "2.6.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" 100 | dependencies = [ 101 | "actix-rt", 102 | "actix-service", 103 | "actix-utils", 104 | "futures-core", 105 | "futures-util", 106 | "mio", 107 | "socket2", 108 | "tokio", 109 | "tracing", 110 | ] 111 | 112 | [[package]] 113 | name = "actix-service" 114 | version = "2.0.3" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" 117 | dependencies = [ 118 | "futures-core", 119 | "pin-project-lite", 120 | ] 121 | 122 | [[package]] 123 | name = "actix-tls" 124 | version = "3.4.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" 127 | dependencies = [ 128 | "actix-rt", 129 | "actix-service", 130 | "actix-utils", 131 | "futures-core", 132 | "http 0.2.12", 133 | "http 1.3.1", 134 | "impl-more", 135 | "pin-project-lite", 136 | "tokio", 137 | "tokio-rustls", 138 | "tokio-util", 139 | "tracing", 140 | "webpki-roots", 141 | ] 142 | 143 | [[package]] 144 | name = "actix-utils" 145 | version = "3.0.1" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 148 | dependencies = [ 149 | "local-waker", 150 | "pin-project-lite", 151 | ] 152 | 153 | [[package]] 154 | name = "actix-web" 155 | version = "4.11.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" 158 | dependencies = [ 159 | "actix-codec", 160 | "actix-http", 161 | "actix-macros", 162 | "actix-router", 163 | "actix-rt", 164 | "actix-server", 165 | "actix-service", 166 | "actix-tls", 167 | "actix-utils", 168 | "actix-web-codegen", 169 | "bytes", 170 | "bytestring", 171 | "cfg-if", 172 | "cookie", 173 | "derive_more 2.0.1", 174 | "encoding_rs", 175 | "foldhash", 176 | "futures-core", 177 | "futures-util", 178 | "impl-more", 179 | "itoa", 180 | "language-tags", 181 | "log", 182 | "mime", 183 | "once_cell", 184 | "pin-project-lite", 185 | "regex-lite", 186 | "serde", 187 | "serde_json", 188 | "serde_urlencoded", 189 | "smallvec", 190 | "socket2", 191 | "time", 192 | "tracing", 193 | "url", 194 | ] 195 | 196 | [[package]] 197 | name = "actix-web-codegen" 198 | version = "4.3.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" 201 | dependencies = [ 202 | "actix-router", 203 | "proc-macro2", 204 | "quote", 205 | "syn", 206 | ] 207 | 208 | [[package]] 209 | name = "addr2line" 210 | version = "0.24.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 213 | dependencies = [ 214 | "gimli", 215 | ] 216 | 217 | [[package]] 218 | name = "adler2" 219 | version = "2.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 222 | 223 | [[package]] 224 | name = "aho-corasick" 225 | version = "1.1.3" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 228 | dependencies = [ 229 | "memchr", 230 | ] 231 | 232 | [[package]] 233 | name = "alloc-no-stdlib" 234 | version = "2.0.4" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 237 | 238 | [[package]] 239 | name = "alloc-stdlib" 240 | version = "0.2.2" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 243 | dependencies = [ 244 | "alloc-no-stdlib", 245 | ] 246 | 247 | [[package]] 248 | name = "anstream" 249 | version = "0.6.18" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 252 | dependencies = [ 253 | "anstyle", 254 | "anstyle-parse", 255 | "anstyle-query", 256 | "anstyle-wincon", 257 | "colorchoice", 258 | "is_terminal_polyfill", 259 | "utf8parse", 260 | ] 261 | 262 | [[package]] 263 | name = "anstyle" 264 | version = "1.0.10" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 267 | 268 | [[package]] 269 | name = "anstyle-parse" 270 | version = "0.2.6" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 273 | dependencies = [ 274 | "utf8parse", 275 | ] 276 | 277 | [[package]] 278 | name = "anstyle-query" 279 | version = "1.1.2" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 282 | dependencies = [ 283 | "windows-sys 0.59.0", 284 | ] 285 | 286 | [[package]] 287 | name = "anstyle-wincon" 288 | version = "3.0.8" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 291 | dependencies = [ 292 | "anstyle", 293 | "once_cell_polyfill", 294 | "windows-sys 0.59.0", 295 | ] 296 | 297 | [[package]] 298 | name = "askama" 299 | version = "0.12.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" 302 | dependencies = [ 303 | "askama_derive", 304 | "askama_escape", 305 | "percent-encoding", 306 | ] 307 | 308 | [[package]] 309 | name = "askama_derive" 310 | version = "0.12.5" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" 313 | dependencies = [ 314 | "askama_parser", 315 | "mime", 316 | "mime_guess", 317 | "proc-macro2", 318 | "quote", 319 | "syn", 320 | ] 321 | 322 | [[package]] 323 | name = "askama_escape" 324 | version = "0.10.3" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" 327 | 328 | [[package]] 329 | name = "askama_parser" 330 | version = "0.2.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" 333 | dependencies = [ 334 | "nom", 335 | ] 336 | 337 | [[package]] 338 | name = "autocfg" 339 | version = "1.4.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 342 | 343 | [[package]] 344 | name = "awc" 345 | version = "3.7.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "e76d68b4f02400c2f9110437f254873e8f265b35ea87352f142bc7c8e878115a" 348 | dependencies = [ 349 | "actix-codec", 350 | "actix-http", 351 | "actix-rt", 352 | "actix-service", 353 | "actix-tls", 354 | "actix-utils", 355 | "base64 0.22.1", 356 | "bytes", 357 | "cfg-if", 358 | "derive_more 2.0.1", 359 | "futures-core", 360 | "futures-util", 361 | "h2", 362 | "http 0.2.12", 363 | "itoa", 364 | "log", 365 | "mime", 366 | "percent-encoding", 367 | "pin-project-lite", 368 | "rand 0.9.1", 369 | "rustls", 370 | "serde", 371 | "serde_json", 372 | "serde_urlencoded", 373 | "tokio", 374 | ] 375 | 376 | [[package]] 377 | name = "backtrace" 378 | version = "0.3.75" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 381 | dependencies = [ 382 | "addr2line", 383 | "cfg-if", 384 | "libc", 385 | "miniz_oxide", 386 | "object", 387 | "rustc-demangle", 388 | "windows-targets", 389 | ] 390 | 391 | [[package]] 392 | name = "base64" 393 | version = "0.21.7" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 396 | 397 | [[package]] 398 | name = "base64" 399 | version = "0.22.1" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 402 | 403 | [[package]] 404 | name = "bitflags" 405 | version = "1.3.2" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 408 | 409 | [[package]] 410 | name = "bitflags" 411 | version = "2.9.1" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 414 | 415 | [[package]] 416 | name = "block-buffer" 417 | version = "0.10.4" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 420 | dependencies = [ 421 | "generic-array", 422 | ] 423 | 424 | [[package]] 425 | name = "brotli" 426 | version = "8.0.1" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" 429 | dependencies = [ 430 | "alloc-no-stdlib", 431 | "alloc-stdlib", 432 | "brotli-decompressor", 433 | ] 434 | 435 | [[package]] 436 | name = "brotli-decompressor" 437 | version = "5.0.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 440 | dependencies = [ 441 | "alloc-no-stdlib", 442 | "alloc-stdlib", 443 | ] 444 | 445 | [[package]] 446 | name = "bytemuck" 447 | version = "1.23.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 450 | 451 | [[package]] 452 | name = "byteorder" 453 | version = "1.5.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 456 | 457 | [[package]] 458 | name = "byteorder-lite" 459 | version = "0.1.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 462 | 463 | [[package]] 464 | name = "bytes" 465 | version = "1.10.1" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 468 | 469 | [[package]] 470 | name = "bytestring" 471 | version = "1.4.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" 474 | dependencies = [ 475 | "bytes", 476 | ] 477 | 478 | [[package]] 479 | name = "cc" 480 | version = "1.2.23" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" 483 | dependencies = [ 484 | "shlex", 485 | ] 486 | 487 | [[package]] 488 | name = "cfg-if" 489 | version = "1.0.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 492 | 493 | [[package]] 494 | name = "clap" 495 | version = "4.5.38" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 498 | dependencies = [ 499 | "clap_builder", 500 | "clap_derive", 501 | ] 502 | 503 | [[package]] 504 | name = "clap_builder" 505 | version = "4.5.38" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 508 | dependencies = [ 509 | "anstream", 510 | "anstyle", 511 | "clap_lex", 512 | "strsim", 513 | ] 514 | 515 | [[package]] 516 | name = "clap_derive" 517 | version = "4.5.32" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 520 | dependencies = [ 521 | "heck", 522 | "proc-macro2", 523 | "quote", 524 | "syn", 525 | ] 526 | 527 | [[package]] 528 | name = "clap_lex" 529 | version = "0.7.4" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 532 | 533 | [[package]] 534 | name = "colorchoice" 535 | version = "1.0.3" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 538 | 539 | [[package]] 540 | name = "cookie" 541 | version = "0.16.2" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 544 | dependencies = [ 545 | "percent-encoding", 546 | "time", 547 | "version_check", 548 | ] 549 | 550 | [[package]] 551 | name = "cpufeatures" 552 | version = "0.2.17" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 555 | dependencies = [ 556 | "libc", 557 | ] 558 | 559 | [[package]] 560 | name = "crc32fast" 561 | version = "1.4.2" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 564 | dependencies = [ 565 | "cfg-if", 566 | ] 567 | 568 | [[package]] 569 | name = "crypto-common" 570 | version = "0.1.6" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 573 | dependencies = [ 574 | "generic-array", 575 | "typenum", 576 | ] 577 | 578 | [[package]] 579 | name = "cssparser" 580 | version = "0.34.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" 583 | dependencies = [ 584 | "cssparser-macros", 585 | "dtoa-short", 586 | "itoa", 587 | "phf", 588 | "smallvec", 589 | ] 590 | 591 | [[package]] 592 | name = "cssparser-macros" 593 | version = "0.6.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" 596 | dependencies = [ 597 | "quote", 598 | "syn", 599 | ] 600 | 601 | [[package]] 602 | name = "deranged" 603 | version = "0.4.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 606 | dependencies = [ 607 | "powerfmt", 608 | ] 609 | 610 | [[package]] 611 | name = "derive_more" 612 | version = "0.99.20" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" 615 | dependencies = [ 616 | "proc-macro2", 617 | "quote", 618 | "syn", 619 | ] 620 | 621 | [[package]] 622 | name = "derive_more" 623 | version = "2.0.1" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 626 | dependencies = [ 627 | "derive_more-impl", 628 | ] 629 | 630 | [[package]] 631 | name = "derive_more-impl" 632 | version = "2.0.1" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 635 | dependencies = [ 636 | "proc-macro2", 637 | "quote", 638 | "syn", 639 | "unicode-xid", 640 | ] 641 | 642 | [[package]] 643 | name = "digest" 644 | version = "0.10.7" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 647 | dependencies = [ 648 | "block-buffer", 649 | "crypto-common", 650 | ] 651 | 652 | [[package]] 653 | name = "displaydoc" 654 | version = "0.2.5" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 657 | dependencies = [ 658 | "proc-macro2", 659 | "quote", 660 | "syn", 661 | ] 662 | 663 | [[package]] 664 | name = "dtoa" 665 | version = "1.0.10" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" 668 | 669 | [[package]] 670 | name = "dtoa-short" 671 | version = "0.3.5" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" 674 | dependencies = [ 675 | "dtoa", 676 | ] 677 | 678 | [[package]] 679 | name = "ego-tree" 680 | version = "0.10.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" 683 | 684 | [[package]] 685 | name = "encoding_rs" 686 | version = "0.8.35" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 689 | dependencies = [ 690 | "cfg-if", 691 | ] 692 | 693 | [[package]] 694 | name = "equivalent" 695 | version = "1.0.2" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 698 | 699 | [[package]] 700 | name = "fastrand" 701 | version = "1.9.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 704 | dependencies = [ 705 | "instant", 706 | ] 707 | 708 | [[package]] 709 | name = "fdeflate" 710 | version = "0.3.7" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 713 | dependencies = [ 714 | "simd-adler32", 715 | ] 716 | 717 | [[package]] 718 | name = "flate2" 719 | version = "1.1.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 722 | dependencies = [ 723 | "crc32fast", 724 | "miniz_oxide", 725 | ] 726 | 727 | [[package]] 728 | name = "fnv" 729 | version = "1.0.7" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 732 | 733 | [[package]] 734 | name = "foldhash" 735 | version = "0.1.5" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 738 | 739 | [[package]] 740 | name = "form_urlencoded" 741 | version = "1.2.1" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 744 | dependencies = [ 745 | "percent-encoding", 746 | ] 747 | 748 | [[package]] 749 | name = "futf" 750 | version = "0.1.5" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 753 | dependencies = [ 754 | "mac", 755 | "new_debug_unreachable", 756 | ] 757 | 758 | [[package]] 759 | name = "futures" 760 | version = "0.3.31" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 763 | dependencies = [ 764 | "futures-channel", 765 | "futures-core", 766 | "futures-io", 767 | "futures-sink", 768 | "futures-task", 769 | "futures-util", 770 | ] 771 | 772 | [[package]] 773 | name = "futures-channel" 774 | version = "0.3.31" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 777 | dependencies = [ 778 | "futures-core", 779 | "futures-sink", 780 | ] 781 | 782 | [[package]] 783 | name = "futures-core" 784 | version = "0.3.31" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 787 | 788 | [[package]] 789 | name = "futures-io" 790 | version = "0.3.31" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 793 | 794 | [[package]] 795 | name = "futures-sink" 796 | version = "0.3.31" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 799 | 800 | [[package]] 801 | name = "futures-task" 802 | version = "0.3.31" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 805 | 806 | [[package]] 807 | name = "futures-util" 808 | version = "0.3.31" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 811 | dependencies = [ 812 | "futures-core", 813 | "futures-sink", 814 | "futures-task", 815 | "pin-project-lite", 816 | "pin-utils", 817 | ] 818 | 819 | [[package]] 820 | name = "fxhash" 821 | version = "0.2.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 824 | dependencies = [ 825 | "byteorder", 826 | ] 827 | 828 | [[package]] 829 | name = "generic-array" 830 | version = "0.14.7" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 833 | dependencies = [ 834 | "typenum", 835 | "version_check", 836 | ] 837 | 838 | [[package]] 839 | name = "getopts" 840 | version = "0.2.21" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 843 | dependencies = [ 844 | "unicode-width", 845 | ] 846 | 847 | [[package]] 848 | name = "getrandom" 849 | version = "0.2.16" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 852 | dependencies = [ 853 | "cfg-if", 854 | "libc", 855 | "wasi 0.11.0+wasi-snapshot-preview1", 856 | ] 857 | 858 | [[package]] 859 | name = "getrandom" 860 | version = "0.3.3" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 863 | dependencies = [ 864 | "cfg-if", 865 | "libc", 866 | "r-efi", 867 | "wasi 0.14.2+wasi-0.2.4", 868 | ] 869 | 870 | [[package]] 871 | name = "gimli" 872 | version = "0.31.1" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 875 | 876 | [[package]] 877 | name = "h2" 878 | version = "0.3.26" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 881 | dependencies = [ 882 | "bytes", 883 | "fnv", 884 | "futures-core", 885 | "futures-sink", 886 | "futures-util", 887 | "http 0.2.12", 888 | "indexmap", 889 | "slab", 890 | "tokio", 891 | "tokio-util", 892 | "tracing", 893 | ] 894 | 895 | [[package]] 896 | name = "hashbrown" 897 | version = "0.15.3" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 900 | 901 | [[package]] 902 | name = "heck" 903 | version = "0.5.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 906 | 907 | [[package]] 908 | name = "html5ever" 909 | version = "0.29.1" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" 912 | dependencies = [ 913 | "log", 914 | "mac", 915 | "markup5ever", 916 | "match_token", 917 | ] 918 | 919 | [[package]] 920 | name = "http" 921 | version = "0.2.12" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 924 | dependencies = [ 925 | "bytes", 926 | "fnv", 927 | "itoa", 928 | ] 929 | 930 | [[package]] 931 | name = "http" 932 | version = "1.3.1" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 935 | dependencies = [ 936 | "bytes", 937 | "fnv", 938 | "itoa", 939 | ] 940 | 941 | [[package]] 942 | name = "httparse" 943 | version = "1.10.1" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 946 | 947 | [[package]] 948 | name = "httpdate" 949 | version = "1.0.3" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 952 | 953 | [[package]] 954 | name = "icu_collections" 955 | version = "2.0.0" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 958 | dependencies = [ 959 | "displaydoc", 960 | "potential_utf", 961 | "yoke", 962 | "zerofrom", 963 | "zerovec", 964 | ] 965 | 966 | [[package]] 967 | name = "icu_locale_core" 968 | version = "2.0.0" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 971 | dependencies = [ 972 | "displaydoc", 973 | "litemap", 974 | "tinystr", 975 | "writeable", 976 | "zerovec", 977 | ] 978 | 979 | [[package]] 980 | name = "icu_normalizer" 981 | version = "2.0.0" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 984 | dependencies = [ 985 | "displaydoc", 986 | "icu_collections", 987 | "icu_normalizer_data", 988 | "icu_properties", 989 | "icu_provider", 990 | "smallvec", 991 | "zerovec", 992 | ] 993 | 994 | [[package]] 995 | name = "icu_normalizer_data" 996 | version = "2.0.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 999 | 1000 | [[package]] 1001 | name = "icu_properties" 1002 | version = "2.0.1" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1005 | dependencies = [ 1006 | "displaydoc", 1007 | "icu_collections", 1008 | "icu_locale_core", 1009 | "icu_properties_data", 1010 | "icu_provider", 1011 | "potential_utf", 1012 | "zerotrie", 1013 | "zerovec", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "icu_properties_data" 1018 | version = "2.0.1" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1021 | 1022 | [[package]] 1023 | name = "icu_provider" 1024 | version = "2.0.0" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1027 | dependencies = [ 1028 | "displaydoc", 1029 | "icu_locale_core", 1030 | "stable_deref_trait", 1031 | "tinystr", 1032 | "writeable", 1033 | "yoke", 1034 | "zerofrom", 1035 | "zerotrie", 1036 | "zerovec", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "idna" 1041 | version = "1.0.3" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1044 | dependencies = [ 1045 | "idna_adapter", 1046 | "smallvec", 1047 | "utf8_iter", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "idna_adapter" 1052 | version = "1.2.1" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1055 | dependencies = [ 1056 | "icu_normalizer", 1057 | "icu_properties", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "image" 1062 | version = "0.25.6" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" 1065 | dependencies = [ 1066 | "bytemuck", 1067 | "byteorder-lite", 1068 | "image-webp", 1069 | "num-traits", 1070 | "png", 1071 | "zune-core", 1072 | "zune-jpeg", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "image-webp" 1077 | version = "0.2.1" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" 1080 | dependencies = [ 1081 | "byteorder-lite", 1082 | "quick-error", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "impl-more" 1087 | version = "0.1.9" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" 1090 | 1091 | [[package]] 1092 | name = "include_dir" 1093 | version = "0.7.4" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" 1096 | dependencies = [ 1097 | "include_dir_macros", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "include_dir_macros" 1102 | version = "0.7.4" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" 1105 | dependencies = [ 1106 | "proc-macro2", 1107 | "quote", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "indexmap" 1112 | version = "2.9.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 1115 | dependencies = [ 1116 | "equivalent", 1117 | "hashbrown", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "instant" 1122 | version = "0.1.13" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 1125 | dependencies = [ 1126 | "cfg-if", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "intellectual" 1131 | version = "0.8.2" 1132 | dependencies = [ 1133 | "actix-web", 1134 | "askama", 1135 | "awc", 1136 | "clap", 1137 | "cookie", 1138 | "futures", 1139 | "image", 1140 | "include_dir", 1141 | "lazy-regex", 1142 | "log", 1143 | "random-string", 1144 | "rustls", 1145 | "rustls-pemfile", 1146 | "scraper", 1147 | "serde", 1148 | "serde_json", 1149 | "simplelog", 1150 | "urlencoding", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "is_terminal_polyfill" 1155 | version = "1.70.1" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1158 | 1159 | [[package]] 1160 | name = "itoa" 1161 | version = "1.0.15" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1164 | 1165 | [[package]] 1166 | name = "language-tags" 1167 | version = "0.3.2" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 1170 | 1171 | [[package]] 1172 | name = "lazy-regex" 1173 | version = "3.4.1" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" 1176 | dependencies = [ 1177 | "lazy-regex-proc_macros", 1178 | "once_cell", 1179 | "regex", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "lazy-regex-proc_macros" 1184 | version = "3.4.1" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" 1187 | dependencies = [ 1188 | "proc-macro2", 1189 | "quote", 1190 | "regex", 1191 | "syn", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "libc" 1196 | version = "0.2.172" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 1199 | 1200 | [[package]] 1201 | name = "litemap" 1202 | version = "0.8.0" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1205 | 1206 | [[package]] 1207 | name = "local-channel" 1208 | version = "0.1.5" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" 1211 | dependencies = [ 1212 | "futures-core", 1213 | "futures-sink", 1214 | "local-waker", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "local-waker" 1219 | version = "0.1.4" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" 1222 | 1223 | [[package]] 1224 | name = "lock_api" 1225 | version = "0.4.12" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1228 | dependencies = [ 1229 | "autocfg", 1230 | "scopeguard", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "log" 1235 | version = "0.4.27" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1238 | 1239 | [[package]] 1240 | name = "mac" 1241 | version = "0.1.1" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 1244 | 1245 | [[package]] 1246 | name = "markup5ever" 1247 | version = "0.14.1" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" 1250 | dependencies = [ 1251 | "log", 1252 | "phf", 1253 | "phf_codegen", 1254 | "string_cache", 1255 | "string_cache_codegen", 1256 | "tendril", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "match_token" 1261 | version = "0.1.0" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" 1264 | dependencies = [ 1265 | "proc-macro2", 1266 | "quote", 1267 | "syn", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "memchr" 1272 | version = "2.7.4" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1275 | 1276 | [[package]] 1277 | name = "mime" 1278 | version = "0.3.17" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1281 | 1282 | [[package]] 1283 | name = "mime_guess" 1284 | version = "2.0.5" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1287 | dependencies = [ 1288 | "mime", 1289 | "unicase", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "minimal-lexical" 1294 | version = "0.2.1" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1297 | 1298 | [[package]] 1299 | name = "miniz_oxide" 1300 | version = "0.8.8" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 1303 | dependencies = [ 1304 | "adler2", 1305 | "simd-adler32", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "mio" 1310 | version = "1.0.3" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1313 | dependencies = [ 1314 | "libc", 1315 | "log", 1316 | "wasi 0.11.0+wasi-snapshot-preview1", 1317 | "windows-sys 0.52.0", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "new_debug_unreachable" 1322 | version = "1.0.6" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 1325 | 1326 | [[package]] 1327 | name = "nom" 1328 | version = "7.1.3" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1331 | dependencies = [ 1332 | "memchr", 1333 | "minimal-lexical", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "num-conv" 1338 | version = "0.1.0" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1341 | 1342 | [[package]] 1343 | name = "num-traits" 1344 | version = "0.2.19" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1347 | dependencies = [ 1348 | "autocfg", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "num_threads" 1353 | version = "0.1.7" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 1356 | dependencies = [ 1357 | "libc", 1358 | ] 1359 | 1360 | [[package]] 1361 | name = "object" 1362 | version = "0.36.7" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1365 | dependencies = [ 1366 | "memchr", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "once_cell" 1371 | version = "1.21.3" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1374 | 1375 | [[package]] 1376 | name = "once_cell_polyfill" 1377 | version = "1.70.1" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1380 | 1381 | [[package]] 1382 | name = "parking_lot" 1383 | version = "0.12.3" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1386 | dependencies = [ 1387 | "lock_api", 1388 | "parking_lot_core", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "parking_lot_core" 1393 | version = "0.9.10" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1396 | dependencies = [ 1397 | "cfg-if", 1398 | "libc", 1399 | "redox_syscall", 1400 | "smallvec", 1401 | "windows-targets", 1402 | ] 1403 | 1404 | [[package]] 1405 | name = "percent-encoding" 1406 | version = "2.3.1" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1409 | 1410 | [[package]] 1411 | name = "phf" 1412 | version = "0.11.3" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 1415 | dependencies = [ 1416 | "phf_macros", 1417 | "phf_shared", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "phf_codegen" 1422 | version = "0.11.3" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 1425 | dependencies = [ 1426 | "phf_generator", 1427 | "phf_shared", 1428 | ] 1429 | 1430 | [[package]] 1431 | name = "phf_generator" 1432 | version = "0.11.3" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 1435 | dependencies = [ 1436 | "phf_shared", 1437 | "rand 0.8.5", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "phf_macros" 1442 | version = "0.11.3" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 1445 | dependencies = [ 1446 | "phf_generator", 1447 | "phf_shared", 1448 | "proc-macro2", 1449 | "quote", 1450 | "syn", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "phf_shared" 1455 | version = "0.11.3" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 1458 | dependencies = [ 1459 | "siphasher", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "pin-project-lite" 1464 | version = "0.2.16" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1467 | 1468 | [[package]] 1469 | name = "pin-utils" 1470 | version = "0.1.0" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1473 | 1474 | [[package]] 1475 | name = "png" 1476 | version = "0.17.16" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 1479 | dependencies = [ 1480 | "bitflags 1.3.2", 1481 | "crc32fast", 1482 | "fdeflate", 1483 | "flate2", 1484 | "miniz_oxide", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "potential_utf" 1489 | version = "0.1.2" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1492 | dependencies = [ 1493 | "zerovec", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "powerfmt" 1498 | version = "0.2.0" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1501 | 1502 | [[package]] 1503 | name = "ppv-lite86" 1504 | version = "0.2.21" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1507 | dependencies = [ 1508 | "zerocopy", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "precomputed-hash" 1513 | version = "0.1.1" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1516 | 1517 | [[package]] 1518 | name = "proc-macro2" 1519 | version = "1.0.95" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1522 | dependencies = [ 1523 | "unicode-ident", 1524 | ] 1525 | 1526 | [[package]] 1527 | name = "quick-error" 1528 | version = "2.0.1" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 1531 | 1532 | [[package]] 1533 | name = "quote" 1534 | version = "1.0.40" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1537 | dependencies = [ 1538 | "proc-macro2", 1539 | ] 1540 | 1541 | [[package]] 1542 | name = "r-efi" 1543 | version = "5.2.0" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1546 | 1547 | [[package]] 1548 | name = "rand" 1549 | version = "0.8.5" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1552 | dependencies = [ 1553 | "rand_core 0.6.4", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "rand" 1558 | version = "0.9.1" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1561 | dependencies = [ 1562 | "rand_chacha", 1563 | "rand_core 0.9.3", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "rand_chacha" 1568 | version = "0.9.0" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1571 | dependencies = [ 1572 | "ppv-lite86", 1573 | "rand_core 0.9.3", 1574 | ] 1575 | 1576 | [[package]] 1577 | name = "rand_core" 1578 | version = "0.6.4" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1581 | 1582 | [[package]] 1583 | name = "rand_core" 1584 | version = "0.9.3" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1587 | dependencies = [ 1588 | "getrandom 0.3.3", 1589 | ] 1590 | 1591 | [[package]] 1592 | name = "random-string" 1593 | version = "1.1.0" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "f70fd13c3024ae3f17381bb5c4d409c6dc9ea6895c08fa2147aba305bea3c4af" 1596 | dependencies = [ 1597 | "fastrand", 1598 | ] 1599 | 1600 | [[package]] 1601 | name = "redox_syscall" 1602 | version = "0.5.12" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 1605 | dependencies = [ 1606 | "bitflags 2.9.1", 1607 | ] 1608 | 1609 | [[package]] 1610 | name = "regex" 1611 | version = "1.11.1" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1614 | dependencies = [ 1615 | "aho-corasick", 1616 | "memchr", 1617 | "regex-automata", 1618 | "regex-syntax", 1619 | ] 1620 | 1621 | [[package]] 1622 | name = "regex-automata" 1623 | version = "0.4.9" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1626 | dependencies = [ 1627 | "aho-corasick", 1628 | "memchr", 1629 | "regex-syntax", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "regex-lite" 1634 | version = "0.1.6" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" 1637 | 1638 | [[package]] 1639 | name = "regex-syntax" 1640 | version = "0.8.5" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1643 | 1644 | [[package]] 1645 | name = "ring" 1646 | version = "0.17.14" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1649 | dependencies = [ 1650 | "cc", 1651 | "cfg-if", 1652 | "getrandom 0.2.16", 1653 | "libc", 1654 | "untrusted", 1655 | "windows-sys 0.52.0", 1656 | ] 1657 | 1658 | [[package]] 1659 | name = "rustc-demangle" 1660 | version = "0.1.24" 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" 1662 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1663 | 1664 | [[package]] 1665 | name = "rustls" 1666 | version = "0.21.12" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 1669 | dependencies = [ 1670 | "log", 1671 | "ring", 1672 | "rustls-webpki", 1673 | "sct", 1674 | ] 1675 | 1676 | [[package]] 1677 | name = "rustls-pemfile" 1678 | version = "1.0.4" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1681 | dependencies = [ 1682 | "base64 0.21.7", 1683 | ] 1684 | 1685 | [[package]] 1686 | name = "rustls-webpki" 1687 | version = "0.101.7" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 1690 | dependencies = [ 1691 | "ring", 1692 | "untrusted", 1693 | ] 1694 | 1695 | [[package]] 1696 | name = "ryu" 1697 | version = "1.0.20" 1698 | source = "registry+https://github.com/rust-lang/crates.io-index" 1699 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1700 | 1701 | [[package]] 1702 | name = "scopeguard" 1703 | version = "1.2.0" 1704 | source = "registry+https://github.com/rust-lang/crates.io-index" 1705 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1706 | 1707 | [[package]] 1708 | name = "scraper" 1709 | version = "0.23.1" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2" 1712 | dependencies = [ 1713 | "cssparser", 1714 | "ego-tree", 1715 | "getopts", 1716 | "html5ever", 1717 | "precomputed-hash", 1718 | "selectors", 1719 | "tendril", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "sct" 1724 | version = "0.7.1" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 1727 | dependencies = [ 1728 | "ring", 1729 | "untrusted", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "selectors" 1734 | version = "0.26.0" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" 1737 | dependencies = [ 1738 | "bitflags 2.9.1", 1739 | "cssparser", 1740 | "derive_more 0.99.20", 1741 | "fxhash", 1742 | "log", 1743 | "new_debug_unreachable", 1744 | "phf", 1745 | "phf_codegen", 1746 | "precomputed-hash", 1747 | "servo_arc", 1748 | "smallvec", 1749 | ] 1750 | 1751 | [[package]] 1752 | name = "serde" 1753 | version = "1.0.219" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1756 | dependencies = [ 1757 | "serde_derive", 1758 | ] 1759 | 1760 | [[package]] 1761 | name = "serde_derive" 1762 | version = "1.0.219" 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" 1764 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1765 | dependencies = [ 1766 | "proc-macro2", 1767 | "quote", 1768 | "syn", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "serde_json" 1773 | version = "1.0.140" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1776 | dependencies = [ 1777 | "itoa", 1778 | "memchr", 1779 | "ryu", 1780 | "serde", 1781 | ] 1782 | 1783 | [[package]] 1784 | name = "serde_urlencoded" 1785 | version = "0.7.1" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1788 | dependencies = [ 1789 | "form_urlencoded", 1790 | "itoa", 1791 | "ryu", 1792 | "serde", 1793 | ] 1794 | 1795 | [[package]] 1796 | name = "servo_arc" 1797 | version = "0.4.0" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" 1800 | dependencies = [ 1801 | "stable_deref_trait", 1802 | ] 1803 | 1804 | [[package]] 1805 | name = "sha1" 1806 | version = "0.10.6" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1809 | dependencies = [ 1810 | "cfg-if", 1811 | "cpufeatures", 1812 | "digest", 1813 | ] 1814 | 1815 | [[package]] 1816 | name = "shlex" 1817 | version = "1.3.0" 1818 | source = "registry+https://github.com/rust-lang/crates.io-index" 1819 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1820 | 1821 | [[package]] 1822 | name = "signal-hook-registry" 1823 | version = "1.4.5" 1824 | source = "registry+https://github.com/rust-lang/crates.io-index" 1825 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1826 | dependencies = [ 1827 | "libc", 1828 | ] 1829 | 1830 | [[package]] 1831 | name = "simd-adler32" 1832 | version = "0.3.7" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1835 | 1836 | [[package]] 1837 | name = "simplelog" 1838 | version = "0.12.2" 1839 | source = "registry+https://github.com/rust-lang/crates.io-index" 1840 | checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" 1841 | dependencies = [ 1842 | "log", 1843 | "termcolor", 1844 | "time", 1845 | ] 1846 | 1847 | [[package]] 1848 | name = "siphasher" 1849 | version = "1.0.1" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1852 | 1853 | [[package]] 1854 | name = "slab" 1855 | version = "0.4.9" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1858 | dependencies = [ 1859 | "autocfg", 1860 | ] 1861 | 1862 | [[package]] 1863 | name = "smallvec" 1864 | version = "1.15.0" 1865 | source = "registry+https://github.com/rust-lang/crates.io-index" 1866 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1867 | 1868 | [[package]] 1869 | name = "socket2" 1870 | version = "0.5.9" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1873 | dependencies = [ 1874 | "libc", 1875 | "windows-sys 0.52.0", 1876 | ] 1877 | 1878 | [[package]] 1879 | name = "stable_deref_trait" 1880 | version = "1.2.0" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1883 | 1884 | [[package]] 1885 | name = "string_cache" 1886 | version = "0.8.9" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" 1889 | dependencies = [ 1890 | "new_debug_unreachable", 1891 | "parking_lot", 1892 | "phf_shared", 1893 | "precomputed-hash", 1894 | "serde", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "string_cache_codegen" 1899 | version = "0.5.4" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 1902 | dependencies = [ 1903 | "phf_generator", 1904 | "phf_shared", 1905 | "proc-macro2", 1906 | "quote", 1907 | ] 1908 | 1909 | [[package]] 1910 | name = "strsim" 1911 | version = "0.11.1" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1914 | 1915 | [[package]] 1916 | name = "syn" 1917 | version = "2.0.101" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 1920 | dependencies = [ 1921 | "proc-macro2", 1922 | "quote", 1923 | "unicode-ident", 1924 | ] 1925 | 1926 | [[package]] 1927 | name = "synstructure" 1928 | version = "0.13.2" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1931 | dependencies = [ 1932 | "proc-macro2", 1933 | "quote", 1934 | "syn", 1935 | ] 1936 | 1937 | [[package]] 1938 | name = "tendril" 1939 | version = "0.4.3" 1940 | source = "registry+https://github.com/rust-lang/crates.io-index" 1941 | checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 1942 | dependencies = [ 1943 | "futf", 1944 | "mac", 1945 | "utf-8", 1946 | ] 1947 | 1948 | [[package]] 1949 | name = "termcolor" 1950 | version = "1.4.1" 1951 | source = "registry+https://github.com/rust-lang/crates.io-index" 1952 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1953 | dependencies = [ 1954 | "winapi-util", 1955 | ] 1956 | 1957 | [[package]] 1958 | name = "time" 1959 | version = "0.3.41" 1960 | source = "registry+https://github.com/rust-lang/crates.io-index" 1961 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1962 | dependencies = [ 1963 | "deranged", 1964 | "itoa", 1965 | "libc", 1966 | "num-conv", 1967 | "num_threads", 1968 | "powerfmt", 1969 | "serde", 1970 | "time-core", 1971 | "time-macros", 1972 | ] 1973 | 1974 | [[package]] 1975 | name = "time-core" 1976 | version = "0.1.4" 1977 | source = "registry+https://github.com/rust-lang/crates.io-index" 1978 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1979 | 1980 | [[package]] 1981 | name = "time-macros" 1982 | version = "0.2.22" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 1985 | dependencies = [ 1986 | "num-conv", 1987 | "time-core", 1988 | ] 1989 | 1990 | [[package]] 1991 | name = "tinystr" 1992 | version = "0.8.1" 1993 | source = "registry+https://github.com/rust-lang/crates.io-index" 1994 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1995 | dependencies = [ 1996 | "displaydoc", 1997 | "zerovec", 1998 | ] 1999 | 2000 | [[package]] 2001 | name = "tokio" 2002 | version = "1.45.0" 2003 | source = "registry+https://github.com/rust-lang/crates.io-index" 2004 | checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 2005 | dependencies = [ 2006 | "backtrace", 2007 | "bytes", 2008 | "libc", 2009 | "mio", 2010 | "parking_lot", 2011 | "pin-project-lite", 2012 | "signal-hook-registry", 2013 | "socket2", 2014 | "windows-sys 0.52.0", 2015 | ] 2016 | 2017 | [[package]] 2018 | name = "tokio-rustls" 2019 | version = "0.24.1" 2020 | source = "registry+https://github.com/rust-lang/crates.io-index" 2021 | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 2022 | dependencies = [ 2023 | "rustls", 2024 | "tokio", 2025 | ] 2026 | 2027 | [[package]] 2028 | name = "tokio-util" 2029 | version = "0.7.15" 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" 2031 | checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 2032 | dependencies = [ 2033 | "bytes", 2034 | "futures-core", 2035 | "futures-sink", 2036 | "pin-project-lite", 2037 | "tokio", 2038 | ] 2039 | 2040 | [[package]] 2041 | name = "tracing" 2042 | version = "0.1.41" 2043 | source = "registry+https://github.com/rust-lang/crates.io-index" 2044 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2045 | dependencies = [ 2046 | "log", 2047 | "pin-project-lite", 2048 | "tracing-attributes", 2049 | "tracing-core", 2050 | ] 2051 | 2052 | [[package]] 2053 | name = "tracing-attributes" 2054 | version = "0.1.28" 2055 | source = "registry+https://github.com/rust-lang/crates.io-index" 2056 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 2057 | dependencies = [ 2058 | "proc-macro2", 2059 | "quote", 2060 | "syn", 2061 | ] 2062 | 2063 | [[package]] 2064 | name = "tracing-core" 2065 | version = "0.1.33" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 2068 | dependencies = [ 2069 | "once_cell", 2070 | ] 2071 | 2072 | [[package]] 2073 | name = "typenum" 2074 | version = "1.18.0" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2077 | 2078 | [[package]] 2079 | name = "unicase" 2080 | version = "2.8.1" 2081 | source = "registry+https://github.com/rust-lang/crates.io-index" 2082 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2083 | 2084 | [[package]] 2085 | name = "unicode-ident" 2086 | version = "1.0.18" 2087 | source = "registry+https://github.com/rust-lang/crates.io-index" 2088 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2089 | 2090 | [[package]] 2091 | name = "unicode-width" 2092 | version = "0.1.14" 2093 | source = "registry+https://github.com/rust-lang/crates.io-index" 2094 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 2095 | 2096 | [[package]] 2097 | name = "unicode-xid" 2098 | version = "0.2.6" 2099 | source = "registry+https://github.com/rust-lang/crates.io-index" 2100 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 2101 | 2102 | [[package]] 2103 | name = "untrusted" 2104 | version = "0.9.0" 2105 | source = "registry+https://github.com/rust-lang/crates.io-index" 2106 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2107 | 2108 | [[package]] 2109 | name = "url" 2110 | version = "2.5.4" 2111 | source = "registry+https://github.com/rust-lang/crates.io-index" 2112 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2113 | dependencies = [ 2114 | "form_urlencoded", 2115 | "idna", 2116 | "percent-encoding", 2117 | ] 2118 | 2119 | [[package]] 2120 | name = "urlencoding" 2121 | version = "2.1.3" 2122 | source = "registry+https://github.com/rust-lang/crates.io-index" 2123 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 2124 | 2125 | [[package]] 2126 | name = "utf-8" 2127 | version = "0.7.6" 2128 | source = "registry+https://github.com/rust-lang/crates.io-index" 2129 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2130 | 2131 | [[package]] 2132 | name = "utf8_iter" 2133 | version = "1.0.4" 2134 | source = "registry+https://github.com/rust-lang/crates.io-index" 2135 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2136 | 2137 | [[package]] 2138 | name = "utf8parse" 2139 | version = "0.2.2" 2140 | source = "registry+https://github.com/rust-lang/crates.io-index" 2141 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2142 | 2143 | [[package]] 2144 | name = "version_check" 2145 | version = "0.9.5" 2146 | source = "registry+https://github.com/rust-lang/crates.io-index" 2147 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2148 | 2149 | [[package]] 2150 | name = "wasi" 2151 | version = "0.11.0+wasi-snapshot-preview1" 2152 | source = "registry+https://github.com/rust-lang/crates.io-index" 2153 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2154 | 2155 | [[package]] 2156 | name = "wasi" 2157 | version = "0.14.2+wasi-0.2.4" 2158 | source = "registry+https://github.com/rust-lang/crates.io-index" 2159 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 2160 | dependencies = [ 2161 | "wit-bindgen-rt", 2162 | ] 2163 | 2164 | [[package]] 2165 | name = "webpki-roots" 2166 | version = "0.25.4" 2167 | source = "registry+https://github.com/rust-lang/crates.io-index" 2168 | checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" 2169 | 2170 | [[package]] 2171 | name = "winapi-util" 2172 | version = "0.1.9" 2173 | source = "registry+https://github.com/rust-lang/crates.io-index" 2174 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2175 | dependencies = [ 2176 | "windows-sys 0.59.0", 2177 | ] 2178 | 2179 | [[package]] 2180 | name = "windows-sys" 2181 | version = "0.52.0" 2182 | source = "registry+https://github.com/rust-lang/crates.io-index" 2183 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2184 | dependencies = [ 2185 | "windows-targets", 2186 | ] 2187 | 2188 | [[package]] 2189 | name = "windows-sys" 2190 | version = "0.59.0" 2191 | source = "registry+https://github.com/rust-lang/crates.io-index" 2192 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2193 | dependencies = [ 2194 | "windows-targets", 2195 | ] 2196 | 2197 | [[package]] 2198 | name = "windows-targets" 2199 | version = "0.52.6" 2200 | source = "registry+https://github.com/rust-lang/crates.io-index" 2201 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2202 | dependencies = [ 2203 | "windows_aarch64_gnullvm", 2204 | "windows_aarch64_msvc", 2205 | "windows_i686_gnu", 2206 | "windows_i686_gnullvm", 2207 | "windows_i686_msvc", 2208 | "windows_x86_64_gnu", 2209 | "windows_x86_64_gnullvm", 2210 | "windows_x86_64_msvc", 2211 | ] 2212 | 2213 | [[package]] 2214 | name = "windows_aarch64_gnullvm" 2215 | version = "0.52.6" 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" 2217 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2218 | 2219 | [[package]] 2220 | name = "windows_aarch64_msvc" 2221 | version = "0.52.6" 2222 | source = "registry+https://github.com/rust-lang/crates.io-index" 2223 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2224 | 2225 | [[package]] 2226 | name = "windows_i686_gnu" 2227 | version = "0.52.6" 2228 | source = "registry+https://github.com/rust-lang/crates.io-index" 2229 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2230 | 2231 | [[package]] 2232 | name = "windows_i686_gnullvm" 2233 | version = "0.52.6" 2234 | source = "registry+https://github.com/rust-lang/crates.io-index" 2235 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2236 | 2237 | [[package]] 2238 | name = "windows_i686_msvc" 2239 | version = "0.52.6" 2240 | source = "registry+https://github.com/rust-lang/crates.io-index" 2241 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2242 | 2243 | [[package]] 2244 | name = "windows_x86_64_gnu" 2245 | version = "0.52.6" 2246 | source = "registry+https://github.com/rust-lang/crates.io-index" 2247 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2248 | 2249 | [[package]] 2250 | name = "windows_x86_64_gnullvm" 2251 | version = "0.52.6" 2252 | source = "registry+https://github.com/rust-lang/crates.io-index" 2253 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2254 | 2255 | [[package]] 2256 | name = "windows_x86_64_msvc" 2257 | version = "0.52.6" 2258 | source = "registry+https://github.com/rust-lang/crates.io-index" 2259 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2260 | 2261 | [[package]] 2262 | name = "wit-bindgen-rt" 2263 | version = "0.39.0" 2264 | source = "registry+https://github.com/rust-lang/crates.io-index" 2265 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2266 | dependencies = [ 2267 | "bitflags 2.9.1", 2268 | ] 2269 | 2270 | [[package]] 2271 | name = "writeable" 2272 | version = "0.6.1" 2273 | source = "registry+https://github.com/rust-lang/crates.io-index" 2274 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 2275 | 2276 | [[package]] 2277 | name = "yoke" 2278 | version = "0.8.0" 2279 | source = "registry+https://github.com/rust-lang/crates.io-index" 2280 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 2281 | dependencies = [ 2282 | "serde", 2283 | "stable_deref_trait", 2284 | "yoke-derive", 2285 | "zerofrom", 2286 | ] 2287 | 2288 | [[package]] 2289 | name = "yoke-derive" 2290 | version = "0.8.0" 2291 | source = "registry+https://github.com/rust-lang/crates.io-index" 2292 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 2293 | dependencies = [ 2294 | "proc-macro2", 2295 | "quote", 2296 | "syn", 2297 | "synstructure", 2298 | ] 2299 | 2300 | [[package]] 2301 | name = "zerocopy" 2302 | version = "0.8.25" 2303 | source = "registry+https://github.com/rust-lang/crates.io-index" 2304 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 2305 | dependencies = [ 2306 | "zerocopy-derive", 2307 | ] 2308 | 2309 | [[package]] 2310 | name = "zerocopy-derive" 2311 | version = "0.8.25" 2312 | source = "registry+https://github.com/rust-lang/crates.io-index" 2313 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 2314 | dependencies = [ 2315 | "proc-macro2", 2316 | "quote", 2317 | "syn", 2318 | ] 2319 | 2320 | [[package]] 2321 | name = "zerofrom" 2322 | version = "0.1.6" 2323 | source = "registry+https://github.com/rust-lang/crates.io-index" 2324 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2325 | dependencies = [ 2326 | "zerofrom-derive", 2327 | ] 2328 | 2329 | [[package]] 2330 | name = "zerofrom-derive" 2331 | version = "0.1.6" 2332 | source = "registry+https://github.com/rust-lang/crates.io-index" 2333 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2334 | dependencies = [ 2335 | "proc-macro2", 2336 | "quote", 2337 | "syn", 2338 | "synstructure", 2339 | ] 2340 | 2341 | [[package]] 2342 | name = "zerotrie" 2343 | version = "0.2.2" 2344 | source = "registry+https://github.com/rust-lang/crates.io-index" 2345 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 2346 | dependencies = [ 2347 | "displaydoc", 2348 | "yoke", 2349 | "zerofrom", 2350 | ] 2351 | 2352 | [[package]] 2353 | name = "zerovec" 2354 | version = "0.11.2" 2355 | source = "registry+https://github.com/rust-lang/crates.io-index" 2356 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 2357 | dependencies = [ 2358 | "yoke", 2359 | "zerofrom", 2360 | "zerovec-derive", 2361 | ] 2362 | 2363 | [[package]] 2364 | name = "zerovec-derive" 2365 | version = "0.11.1" 2366 | source = "registry+https://github.com/rust-lang/crates.io-index" 2367 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 2368 | dependencies = [ 2369 | "proc-macro2", 2370 | "quote", 2371 | "syn", 2372 | ] 2373 | 2374 | [[package]] 2375 | name = "zune-core" 2376 | version = "0.4.12" 2377 | source = "registry+https://github.com/rust-lang/crates.io-index" 2378 | checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 2379 | 2380 | [[package]] 2381 | name = "zune-jpeg" 2382 | version = "0.4.14" 2383 | source = "registry+https://github.com/rust-lang/crates.io-index" 2384 | checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" 2385 | dependencies = [ 2386 | "zune-core", 2387 | ] 2388 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intellectual" 3 | description = "Alternate frontend for Genius focused on privacy and simplicity" 4 | version = "0.8.2" 5 | license = "AGPL-3.0" 6 | repository = "https://github.com/Insprill/intellectual" 7 | edition = "2021" 8 | 9 | [dependencies] 10 | actix-web = { version = "4", default-features = false, features = ["macros", "compress-brotli", "compress-gzip", "cookies", "http2", "rustls-0_21"] } # Zstd doesn't compile on aarch64 musl :/ 11 | askama = { version = "0.12", default-features = false, features = ["percent-encoding"] } 12 | awc = { version = "3", default-features = false, features = ["compress-gzip", "rustls-0_21"] } 13 | clap = { version = "4", features = ["derive"] } 14 | cookie = "0.16" # Must stay compatible with the version actix-web is using. 15 | futures = { version = "0.3", default-features = false } 16 | image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"]} 17 | include_dir = "0.7" 18 | lazy-regex = "3" 19 | log = "0.4" 20 | rustls = "0.21" # Must stay compatible with the version actix-web is using. 21 | rustls-pemfile = "1" 22 | scraper = "0.23" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_json = "1" 25 | simplelog = "0.12" 26 | urlencoding = "2" 27 | 28 | [build-dependencies] 29 | random-string = "1" 30 | 31 | [profile.dev.package.image] 32 | opt-level = 1 33 | 34 | [profile.release] 35 | panic = "abort" 36 | codegen-units = 1 37 | strip = true 38 | lto = true 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #################################################################################################### 2 | ## Builder 3 | #################################################################################################### 4 | FROM rust:alpine AS builder 5 | 6 | RUN apk add --no-cache musl-dev 7 | 8 | WORKDIR /intellectual 9 | 10 | COPY . . 11 | 12 | # Set environment variables so the build has git info 13 | RUN export $(cat .env | xargs) && cargo build --release 14 | 15 | #################################################################################################### 16 | ## Final image 17 | #################################################################################################### 18 | FROM alpine:latest 19 | 20 | # Copy our build 21 | COPY --from=builder /intellectual/target/release/intellectual /usr/local/bin/intellectual 22 | 23 | # Use an unprivileged user 24 | RUN adduser --home /nonexistent --no-create-home --disabled-password intellectual 25 | USER intellectual 26 | 27 | # Tell Docker to expose port 8080 28 | EXPOSE 8080/tcp 29 | 30 | # Run a healthcheck every 5 minutes 31 | HEALTHCHECK --interval=5m --timeout=5s CMD wget --tries=1 --spider http://localhost:8080 || exit 1 32 | 33 | CMD ["intellectual"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![Docker Pulls][docker-pulls-shield]][docker-pulls-url] 6 | [![AGPLv3 License][license-shield]][license-url] 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Intellectual

15 |

16 | Alternate frontend for Genius focused on privacy and simplicity 17 |
18 |
19 | Report Bug 20 | · 21 | Request Feature 22 |

23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | Table of Contents 31 |
    32 |
  1. About The Project
  2. 33 |
  3. Instances
  4. 34 |
  5. Deployment
  6. 35 |
  7. Roadmap
  8. 36 |
  9. License
  10. 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | ## About The Project 46 | 47 | Intellectual is an alternative frontend for [Genius](https://genius.com/) focused on privacy and simplicity. 48 | Written in Rust, Intellectual is incredibly lightweight. 49 | Not requiring JavaScript and proxying all requests through the server, including images. 50 | 51 | Intellectual is still very early in development and is lacking many features. 52 | Check out the [roadmap](#roadmap) for what features will be coming next! 53 | 54 | 55 | 56 | 57 | 58 | 59 | ## Instances 60 | 61 | Want your own instance listed here? Open an issue for it! 62 | Not sure how to host your own instance? View the [deployment](#deployment) instructions. 63 | 64 | | URL | Tor | I2P | Country | Cloudflare | 65 | |--------------------------------------------|-------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|-----------|------------| 66 | | https://intellectual.insprill.net/ | No | No | 🇺🇸 US | ✔️ | 67 | | https://intellectual.catsarch.com/ | [Yes](http://intellectual.catsarchywsyuss6jdxlypsw5dc7owd5u5tr6bujxb7o6xw2hipqehyd.onion) | [Yes](http://ahysxi4w2wl7vf7ghy3hbqibvdg4e7je3s2lrsrb6xpukklevrka.b32.i2p) | 🇺🇸 US | | 68 | | https://intellectual.frontendfriendly.xyz/ | No | No | 🇺🇸 US | | 69 | | https://intellectual.lumaeris.com/ | No | No | 🇺🇸 US | | 70 | | https://in.bloat.cat/ | No | No | 🇷🇴 RO | | 71 | | https://in2.bloat.cat/ | No | No | 🇩🇪 DE | | 72 | | https://intellectual.ducks.party/ | [Yes](http://pgsivdkc7p5qyxp7leorxk32mkomepxsmrqhpzqqi2zf2nc6urzodfad.onion) | No | 🇩🇪 DE | | 73 | | https://intellectual.bunk.lol/ | No | No | 🇮🇸 IS | | 74 | | https://genius.blitzw.in/ | No | No | 🇩🇰 DK | | 75 | 76 | If there is a checkmark under "Cloudflare", that means the site 77 | is proxied behind [Cloudflare](https://www.cloudflare.com/). 78 | This means they have the ability to monitor traffic between you and the server. 79 | 80 | The instances list in JSON format can be found in the [instances.json](instances.json) file. 81 | 82 | 83 | 84 | 85 | 86 | 87 | ## Deployment 88 | 89 | ### Deploying 90 | 91 | #### Docker 92 | 93 | The easiest way to host intellectual is via Docker, and the included Docker Compose file. 94 | To create a new directory, download the `docker-compose.yml`, and cd into the directory, run the following command (Requires cURL 7.10.3 or newer) 95 | ```bash 96 | curl https://raw.githubusercontent.com/Insprill/intellectual/master/docker-compose.yml --create-dirs -o intellectual/docker-compose.yml && cd intellectual 97 | ``` 98 | By default, it'll bind to `127.0.0.1:8080`. 99 | Once you're satisfied with the container, you can start it with 100 | ```bash 101 | docker compose up -d 102 | ``` 103 | 104 | #### Native 105 | 106 | If you don't want to use Docker, you can download the latest [stable](https://github.com/Insprill/intellectual/releases) or [nightly](https://nightly.link/Insprill/intellectual/workflows/rust/master) build from GitHub actions. Make sure to choose the version for your target operating system. 107 | 108 | Append the `-h` flag when running to see all available arguments. 109 | 110 | ### TLS 111 | 112 | Intellectual supports TLS connections natively using [rustls][rustls-repo]. 113 | To enable TLS, provide the `--tls` flag, followed by `--tls-key-file` and `--tls-cert-file` pointing to their respective files on disk. 114 | 115 | 116 | 117 | 118 | 119 | 120 | ## Roadmap 121 | 122 | - [x] Search for songs 123 | - [x] View lyrics 124 | - [x] More song info on the lryics page 125 | - Song name 126 | - Song/album image 127 | - Album name 128 | - Artist 129 | - Release date 130 | - [x] View artist info 131 | - [x] Paginated searches 132 | - [x] More robust error handling 133 | - [x] Show artists' work on their page 134 | - [x] Improve responsiveness 135 | - [x] View Albums 136 | - [x] Theme support 137 | - [ ] Annotation support 138 | - [ ] More search types 139 | - By lyrics 140 | - For artists 141 | - For albums 142 | - [ ] Better accessibility 143 | - [ ] Support for more lyric sources 144 | 145 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. 146 | Any contributions you make are **greatly appreciated**! 147 | If you're new to contributing to open-source projects, 148 | you can follow [this](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) guide to get up-to-speed. 149 | 150 | 151 | 152 | 153 | 154 | 155 | ## License 156 | 157 | Distributed under the GNU Affero General Public License v3.0. 158 | See [LICENSE][license-url] for more information. 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | [contributors-shield]: https://img.shields.io/github/contributors/Insprill/intellectual.svg?style=for-the-badge 167 | [contributors-url]: https://github.com/Insprill/intellectual/graphs/contributors 168 | [forks-shield]: https://img.shields.io/github/forks/Insprill/intellectual.svg?style=for-the-badge 169 | [forks-url]: https://github.com/Insprill/intellectual/network/members 170 | [stars-shield]: https://img.shields.io/github/stars/Insprill/intellectual.svg?style=for-the-badge 171 | [stars-url]: https://github.com/Insprill/intellectual/stargazers 172 | [issues-shield]: https://img.shields.io/github/issues/Insprill/intellectual.svg?style=for-the-badge 173 | [issues-url]: https://github.com/Insprill/intellectual/issues 174 | [license-shield]: https://img.shields.io/github/license/Insprill/intellectual.svg?style=for-the-badge 175 | [license-url]: https://github.com/Insprill/intellectual/blob/master/LICENSE 176 | [docker-pulls-shield]: https://img.shields.io/docker/pulls/insprill/intellectual?style=for-the-badge 177 | [docker-pulls-url]: https://hub.docker.com/r/insprill/intellectual 178 | [rustls-repo]: https://github.com/rustls/rustls 179 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use random_string::charsets::ALPHANUMERIC; 2 | use random_string::generate; 3 | use std::env; 4 | use std::process::Command; 5 | 6 | fn main() { 7 | set_version(); 8 | set_repo_url(); 9 | } 10 | 11 | fn set_repo_url() { 12 | let cargo_repo_url = env!("CARGO_PKG_REPOSITORY"); 13 | let mut repo_url = cargo_repo_url.to_string(); 14 | 15 | if is_git_repo() { 16 | repo_url = get_remote_url(); 17 | if repo_url.starts_with("https://github.com/") { 18 | // The URL might already end with a '/', but GitHub seems to handle it fine if there's two. 19 | // Tested in both Firefox and Chromium. 20 | repo_url.push_str(&format!( 21 | "/tree/{}", 22 | if is_git_tagged() { 23 | get_git_tag() 24 | } else { 25 | get_git_hash() 26 | } 27 | )); 28 | } 29 | } else { 30 | println!("Not a Git repo! Skipping repo URL metadata"); 31 | } 32 | 33 | println!("cargo:rustc-env=IN_REPO_URL={}", repo_url); 34 | } 35 | 36 | fn set_version() { 37 | let cargo_version = env!("CARGO_PKG_VERSION"); 38 | let mut version = cargo_version.to_string(); 39 | 40 | if is_git_repo() { 41 | let is_repo_dirty = is_repo_dirty(); 42 | if !is_git_tagged() || is_repo_dirty { 43 | version.push_str("+rev."); 44 | version.push_str(&get_git_hash()); 45 | 46 | // Add some randomness if dirty to avoid the browser caching resources while iterating. 47 | if is_repo_dirty { 48 | version.push_str("-dirty-"); 49 | version.push_str(&generate(4, ALPHANUMERIC)); 50 | } 51 | } 52 | } else { 53 | println!("Not a Git repo! Skipping version metadata"); 54 | } 55 | 56 | println!("cargo:rustc-env=IN_VERSION={}", version); 57 | } 58 | 59 | fn is_git_repo() -> bool { 60 | if let Ok(var) = env::var("IN_IS_GIT") { 61 | return var == "true"; 62 | } 63 | Command::new("git").args(["status"]).status().is_ok() 64 | } 65 | 66 | fn get_git_hash() -> String { 67 | if let Ok(var) = env::var("IN_GIT_HASH") { 68 | return var; 69 | } 70 | 71 | let output = Command::new("git") 72 | .args(["rev-parse", "--short", "HEAD"]) 73 | .output() 74 | .expect("Failed to execute git command"); 75 | 76 | String::from_utf8_lossy(&output.stdout).trim().to_string() 77 | } 78 | 79 | fn is_repo_dirty() -> bool { 80 | if let Ok(var) = env::var("IN_GIT_DIRTY") { 81 | return var == "true"; 82 | } 83 | 84 | let status_output = Command::new("git") 85 | .args(["status", "--porcelain"]) 86 | .output() 87 | .expect("Failed to execute git status command"); 88 | 89 | !status_output.stdout.is_empty() 90 | } 91 | 92 | fn is_git_tagged() -> bool { 93 | if let Ok(var) = env::var("IN_GIT_TAGGED") { 94 | return var == "true"; 95 | } 96 | 97 | get_git_tag_info().0 98 | } 99 | 100 | fn get_git_tag() -> String { 101 | if let Ok(var) = env::var("IN_GIT_TAG") { 102 | return var; 103 | } 104 | 105 | get_git_tag_info().1 106 | } 107 | 108 | fn get_git_tag_info() -> (bool, String) { 109 | let output = Command::new("git") 110 | .args(["describe", "--tags", "--exact-match"]) 111 | .output() 112 | .expect("Failed to execute git describe command"); 113 | 114 | let is_tagged = output.status.success(); 115 | let tag = String::from_utf8_lossy(&output.stdout).trim().to_string(); 116 | 117 | (is_tagged, tag) 118 | } 119 | 120 | fn get_remote_url() -> String { 121 | if let Ok(var) = env::var("IN_GIT_REMOTE_URL") { 122 | return var; 123 | } 124 | 125 | let remote_url = Command::new("git") 126 | .args(["remote", "get-url", "origin"]) 127 | .output() 128 | .expect("Failed to execute git command"); 129 | 130 | String::from_utf8_lossy(&remote_url.stdout) 131 | .trim() 132 | .to_string() 133 | } 134 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | intellectual: 3 | image: insprill/intellectual:latest # Switch to 'develop' to use the latest git version. May be unstable! 4 | restart: always 5 | ports: 6 | - 8080:8080 # Use `127.0.0.1:8080:8080` if you're using a reverse proxy on the same machine. 7 | read_only: true 8 | security_opt: 9 | - no-new-privileges:true 10 | cap_drop: 11 | - ALL 12 | healthcheck: 13 | test: wget -nv --tries=1 --spider http://127.0.0.1:8080 || exit 1 14 | interval: 1m 15 | timeout: 5s 16 | -------------------------------------------------------------------------------- /instances.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "clearnet": "https://intellectual.insprill.net/", 4 | "country": "US", 5 | "cdn": true 6 | }, 7 | { 8 | "clearnet": "https://intellectual.catsarch.com/", 9 | "tor": "http://intellectual.catsarchywsyuss6jdxlypsw5dc7owd5u5tr6bujxb7o6xw2hipqehyd.onion/", 10 | "i2p": "http://ahysxi4w2wl7vf7ghy3hbqibvdg4e7je3s2lrsrb6xpukklevrka.b32.i2p", 11 | "country": "US", 12 | "cdn": false 13 | }, 14 | { 15 | "clearnet": "https://intellectual.frontendfriendly.xyz/", 16 | "country": "US", 17 | "cdn": false 18 | }, 19 | { 20 | "clearnet": "https://intellectual.lumaeris.com/", 21 | "country": "US", 22 | "cdn": false 23 | }, 24 | { 25 | "clearnet": "https://in.bloat.cat/", 26 | "country": "RO", 27 | "cdn": false 28 | }, 29 | { 30 | "clearnet": "https://in2.bloat.cat/", 31 | "country": "DE", 32 | "cdn": false 33 | }, 34 | { 35 | "clearnet": "https://intellectual.ducks.party/", 36 | "tor": "http://pgsivdkc7p5qyxp7leorxk32mkomepxsmrqhpzqqi2zf2nc6urzodfad.onion/", 37 | "country": "DE", 38 | "cdn": false 39 | }, 40 | { 41 | "clearnet": "https://intellectual.bunk.lol/", 42 | "country": "IS", 43 | "cdn": false 44 | }, 45 | { 46 | "clearnet": "https://genius.blitzw.in/", 47 | "country": "DK", 48 | "cdn": false 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/album.rs: -------------------------------------------------------------------------------- 1 | use crate::genius::{self, GeniusAlbumResponse}; 2 | use crate::settings::{settings_from_req, Settings}; 3 | use crate::utils; 4 | use actix_web::HttpRequest; 5 | use actix_web::{get, Responder, Result}; 6 | use askama::Template; 7 | 8 | use crate::genius::GeniusAlbum; 9 | use crate::templates::template; 10 | 11 | #[derive(Template)] 12 | #[template(path = "album.html")] 13 | struct AlbumTemplate { 14 | settings: Settings, 15 | album: GeniusAlbum, 16 | } 17 | 18 | #[get("/albums/{name:.*}")] 19 | pub async fn album(req: HttpRequest) -> Result { 20 | let mut album = genius::extract_data::(req.path()) 21 | .await? 22 | .album; 23 | 24 | album.tracks = Some(genius::get_album_tracks(album.id).await?); 25 | 26 | Ok(template( 27 | &req, 28 | AlbumTemplate { 29 | settings: settings_from_req(&req), 30 | album, 31 | }, 32 | )) 33 | } 34 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufWriter, Cursor}; 2 | 3 | use ::image::{imageops::FilterType, load_from_memory, EncodableLayout, ImageFormat}; 4 | use actix_web::{get, http::header, http::StatusCode, web, HttpRequest, HttpResponse, Responder}; 5 | use serde::Deserialize; 6 | 7 | use crate::genius::{self, SubDomain}; 8 | use crate::Result; 9 | 10 | #[derive(Debug, Deserialize)] 11 | pub struct UrlQuery { 12 | url: String, 13 | size: Option, 14 | } 15 | 16 | #[get("/api/image")] 17 | pub async fn image(req: HttpRequest, info: web::Query) -> Result { 18 | let img_path = match info.url.split('/').next_back() { 19 | Some(path) => path, 20 | None => return Ok(HttpResponse::BadRequest().finish()), 21 | }; 22 | 23 | let (status, body, _) = genius::get_raw(SubDomain::Images, img_path, None).await?; 24 | 25 | if status != StatusCode::OK { 26 | return Ok(HttpResponse::build(status).finish()); 27 | } 28 | 29 | let webp = req 30 | .headers() 31 | .get(header::ACCEPT) 32 | .map(|value| value.to_str().unwrap_or_default().contains("webp")) 33 | .unwrap_or(false); 34 | 35 | let size = info.size.unwrap_or(150).clamp(1, 500).into(); 36 | 37 | if let Ok(abstract_image) = load_from_memory(body.as_bytes()) { 38 | // Images typically aren't smaller than 2kb 39 | let mut buf = BufWriter::new(Cursor::new(Vec::with_capacity(2048))); 40 | let resized = abstract_image.resize_exact(size, size, FilterType::Nearest); 41 | 42 | let image_format = if webp { 43 | ImageFormat::WebP 44 | } else { 45 | ImageFormat::Jpeg 46 | }; 47 | 48 | if resized.write_to(&mut buf, image_format).is_ok() { 49 | let bytes = buf.into_inner().unwrap().into_inner(); // Should never error 50 | return Ok(HttpResponse::Ok() 51 | .append_header(("Cache-Control", "public, max-age=31536000, immutable")) 52 | .append_header(( 53 | "Content-Type", 54 | if webp { "image/webp" } else { "image/jpeg" }, 55 | )) 56 | .body(bytes)); 57 | } 58 | } 59 | 60 | Ok(HttpResponse::InternalServerError().finish()) 61 | } 62 | -------------------------------------------------------------------------------- /src/artist.rs: -------------------------------------------------------------------------------- 1 | use crate::settings::{settings_from_req, Settings}; 2 | use crate::utils; 3 | use actix_web::{get, HttpRequest, Responder, Result}; 4 | use askama::Template; 5 | use lazy_regex::*; 6 | use regex::Regex; 7 | 8 | use crate::genius::{self, GeniusArtist}; 9 | use crate::genius::{GeniusArtistResponse, SortMode}; 10 | use crate::templates::template; 11 | 12 | static GENIUS_IMAGE_URL: &str = "https://images.genius.com/"; 13 | static GENIUS_BASE_PATTERN: Lazy = lazy_regex!(r#"https?://\w*.?genius\.com/"#); 14 | 15 | #[derive(Template)] 16 | #[template(path = "artist.html")] 17 | struct ArtistTemplate { 18 | settings: Settings, 19 | artist: GeniusArtist, 20 | } 21 | 22 | const MAX_SONGS: u8 = 5; 23 | 24 | #[get("/artists/{name}")] 25 | pub async fn artist(req: HttpRequest) -> Result { 26 | let mut artist = genius::extract_data::(req.path()) 27 | .await? 28 | .artist; 29 | 30 | artist.popular_songs = 31 | Some(genius::get_artist_songs(artist.id, SortMode::Popularity, MAX_SONGS).await?); 32 | 33 | if let Some(description) = artist.description.as_mut() { 34 | description.html = rewrite_links(&description.html); 35 | } 36 | 37 | Ok(template( 38 | &req, 39 | ArtistTemplate { 40 | settings: settings_from_req(&req), 41 | artist, 42 | }, 43 | )) 44 | } 45 | 46 | fn rewrite_links(html: &str) -> String { 47 | let html = html.replace( 48 | GENIUS_IMAGE_URL, 49 | &format!("/api/image?url={}", GENIUS_IMAGE_URL), 50 | ); // Images 51 | let html = GENIUS_BASE_PATTERN.replace_all(&html, ""); // We follow Genius' schema 52 | html.to_string() 53 | } 54 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | self, 3 | dev::ServiceResponse, 4 | http::header::{self}, 5 | middleware::ErrorHandlerResponse, 6 | HttpResponse, Result, 7 | }; 8 | use askama::Template; 9 | use awc::error::HeaderValue; 10 | use log::error; 11 | 12 | use crate::{ 13 | settings::{settings_from_req, Settings}, 14 | templates::template_with_res, 15 | }; 16 | 17 | pub fn render_500(res: ServiceResponse) -> Result> { 18 | let err = get_err_str(&res); 19 | if let Some(str) = &err { 20 | error!("{}", str); 21 | } 22 | 23 | let new_response = template_with_res( 24 | res.request(), 25 | HttpResponse::InternalServerError(), 26 | InternalErrorTemplate { 27 | settings: settings_from_req(res.request()), 28 | err, 29 | }, 30 | ); 31 | create(res, new_response) 32 | } 33 | 34 | pub fn render_404(res: ServiceResponse) -> Result> { 35 | let new_response = template_with_res( 36 | res.request(), 37 | HttpResponse::NotFound(), 38 | NotFoundTemplate { 39 | settings: settings_from_req(res.request()), 40 | }, 41 | ); 42 | create(res, new_response) 43 | } 44 | 45 | pub fn render_400(res: ServiceResponse) -> Result> { 46 | let new_response = template_with_res( 47 | res.request(), 48 | HttpResponse::BadRequest(), 49 | BadRequestTemplate { 50 | settings: settings_from_req(res.request()), 51 | err: get_err_str(&res), 52 | }, 53 | ); 54 | create(res, new_response) 55 | } 56 | 57 | fn create( 58 | res: ServiceResponse, 59 | mut new_response: HttpResponse, 60 | ) -> Result> { 61 | new_response 62 | .headers_mut() 63 | .append(header::CACHE_CONTROL, HeaderValue::from_static("no-store")); 64 | Ok(ErrorHandlerResponse::Response( 65 | ServiceResponse::new(res.into_parts().0, new_response).map_into_right_body(), 66 | )) 67 | } 68 | 69 | fn get_err_str(res: &ServiceResponse) -> Option { 70 | res.response().error().map(|err| err.to_string()) 71 | } 72 | 73 | #[derive(Template)] 74 | #[template(path = "500.html")] 75 | struct InternalErrorTemplate { 76 | settings: Settings, 77 | err: Option, 78 | } 79 | 80 | #[derive(Template)] 81 | #[template(path = "404.html")] 82 | struct NotFoundTemplate { 83 | settings: Settings, 84 | } 85 | 86 | #[derive(Template)] 87 | #[template(path = "400.html")] 88 | struct BadRequestTemplate { 89 | settings: Settings, 90 | err: Option, 91 | } 92 | -------------------------------------------------------------------------------- /src/genius.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use crate::Result; 4 | use actix_web::{ 5 | http::{header::HeaderMap, StatusCode}, 6 | web::Bytes, 7 | }; 8 | use awc::{Client, SendClientRequest}; 9 | use scraper::{Html, Selector}; 10 | use serde::{de::DeserializeOwned, Deserialize}; 11 | use urlencoding::encode; 12 | 13 | static EMBEDDED_INFO_SELECTOR: LazyLock = 14 | LazyLock::new(|| Selector::parse("meta[content]").unwrap()); 15 | 16 | pub async fn extract_data(path: &str) -> Result 17 | where 18 | Res: DeserializeOwned, 19 | { 20 | let page = get_text(SubDomain::Root, path, None).await?; 21 | let document = Html::parse_document(&page); 22 | 23 | Ok(document 24 | .select(&EMBEDDED_INFO_SELECTOR) 25 | .map(|element| element.value().attr("content").unwrap()) // Selector only matches content 26 | .find(|content| content.starts_with("{\"")) // JSON API data 27 | .and_then(|content| serde_json::from_str::(content).ok()) 28 | .ok_or("Failed to extract JSON data")?) 29 | } 30 | 31 | /// https://docs.genius.com/#/artists-songs 32 | pub async fn get_artist_songs( 33 | artist_id: u32, 34 | sort_mode: SortMode, 35 | limit: u8, 36 | ) -> Result> { 37 | Ok(get_json::( 38 | SubDomain::Api, 39 | &format!("artists/{artist_id}/songs"), 40 | Some(vec![sort_mode.to_query(), ("per_page", &limit.to_string())]), 41 | ) 42 | .await? 43 | .response 44 | .songs) 45 | } 46 | 47 | pub async fn get_album_tracks(album_id: u32) -> Result> { 48 | Ok( 49 | get_json::(SubDomain::Api, &format!("albums/{album_id}/tracks"), None) 50 | .await? 51 | .response 52 | .tracks 53 | .into_iter() 54 | .map(|track| track.song) 55 | .collect(), 56 | ) 57 | } 58 | 59 | /// https://docs.genius.com/#/songs-show 60 | pub async fn get_song(song_id: u32) -> Result { 61 | Ok( 62 | get_json::(SubDomain::Api, &format!("songs/{song_id}"), None) 63 | .await? 64 | .response 65 | .song, 66 | ) 67 | } 68 | 69 | /// https://docs.genius.com/#/search-search 70 | pub async fn get_search_results(query: &str, page: u8) -> Result> { 71 | Ok(get_json::( 72 | SubDomain::Api, 73 | "search", 74 | Some(vec![("q", query), ("page", &page.to_string())]), 75 | ) 76 | .await? 77 | .response 78 | .hits 79 | .into_iter() 80 | .map(|x| x.result) 81 | .collect()) 82 | } 83 | 84 | pub async fn get_raw( 85 | subdomain: SubDomain, 86 | path: &str, 87 | queries: Option>, 88 | ) -> Result<(StatusCode, Bytes, HeaderMap)> { 89 | let mut res = build_req(subdomain, path, queries)?.await?; 90 | Ok((res.status(), res.body().await?, res.headers().clone())) 91 | } 92 | 93 | pub async fn get_text( 94 | subdomain: SubDomain, 95 | path: &str, 96 | queries: Option>, 97 | ) -> Result { 98 | let bytes = build_req(subdomain, path, queries)? 99 | .await? 100 | .body() 101 | .await? 102 | .to_vec(); 103 | Ok(String::from_utf8(bytes)?) 104 | } 105 | 106 | async fn get_json( 107 | subdomain: SubDomain, 108 | path: &str, 109 | queries: Option>, 110 | ) -> Result { 111 | Ok(build_req(subdomain, path, queries)? 112 | .await? 113 | .json::() 114 | .await?) 115 | } 116 | 117 | fn build_req( 118 | subdomain: SubDomain, 119 | path: &str, 120 | queries: Option>, 121 | ) -> Result { 122 | let query_str = if let Some(q) = queries { 123 | String::from_iter( 124 | q.iter() 125 | .map(|query| format!("&{}={}", query.0, encode(query.1).into_owned())), 126 | ) 127 | } else { 128 | "".into() 129 | }; 130 | 131 | // Using the api path lets us drop the requirement for an API key. 132 | let path: String = if matches!(subdomain, SubDomain::Api) { 133 | format!("api/{path}") 134 | } else { 135 | path.to_owned() 136 | }; 137 | 138 | let req = Client::default() 139 | .get(format!( 140 | "https://{}genius.com/{}?text_format=plain{}", 141 | subdomain.value(), 142 | path.trim_start_matches('/'), 143 | query_str 144 | )) 145 | .send(); 146 | 147 | Ok(req) 148 | } 149 | 150 | pub enum SubDomain { 151 | Api, 152 | Images, 153 | Root, 154 | } 155 | 156 | impl SubDomain { 157 | fn value(&self) -> &'static str { 158 | match *self { 159 | SubDomain::Images => "images.", 160 | SubDomain::Root => "", 161 | SubDomain::Api => "", 162 | } 163 | } 164 | } 165 | 166 | #[derive(Deserialize)] 167 | pub struct GeniusSearchRequest { 168 | pub response: GeniusSearchResponse, 169 | } 170 | 171 | #[derive(Deserialize)] 172 | pub struct GeniusSearchResponse { 173 | pub hits: Vec, 174 | } 175 | 176 | #[derive(Deserialize)] 177 | pub struct GeniusHit { 178 | pub result: GeniusSong, 179 | } 180 | 181 | #[derive(Deserialize)] 182 | pub struct GeniusSongRequest { 183 | pub response: GeniusSongResponse, 184 | } 185 | 186 | #[derive(Deserialize)] 187 | pub struct GeniusSongResponse { 188 | pub song: GeniusSong, 189 | } 190 | 191 | #[derive(Deserialize)] 192 | pub struct GeniusSongsRequest { 193 | pub response: GeniusSongsResponse, 194 | } 195 | 196 | #[derive(Deserialize)] 197 | pub struct GeniusSongsResponse { 198 | pub songs: Vec, 199 | } 200 | 201 | #[derive(Deserialize)] 202 | pub struct GeniusSong { 203 | pub id: u32, 204 | pub title: String, 205 | pub path: String, 206 | pub header_image_url: String, 207 | pub release_date_for_display: Option, 208 | pub song_art_image_thumbnail_url: String, 209 | pub album: Option, 210 | pub stats: GeniusStats, 211 | pub primary_artist: GeniusArtist, 212 | } 213 | 214 | #[derive(Deserialize)] 215 | pub struct GeniusAlbumResponse { 216 | pub album: GeniusAlbum, 217 | } 218 | 219 | #[derive(Deserialize)] 220 | pub struct GeniusTracksRequest { 221 | pub response: GeniusTracksResponse, 222 | } 223 | 224 | #[derive(Deserialize)] 225 | pub struct GeniusTracksResponse { 226 | pub tracks: Vec, 227 | } 228 | 229 | #[derive(Deserialize)] 230 | pub struct GeniusTrack { 231 | pub song: GeniusSong, 232 | } 233 | 234 | #[derive(Deserialize)] 235 | pub struct GeniusAlbum { 236 | pub name: String, 237 | pub id: u32, 238 | pub url: String, 239 | pub cover_art_url: String, 240 | pub release_date_for_display: Option, 241 | pub tracks: Option>, 242 | pub artist: GeniusArtist, 243 | } 244 | 245 | #[derive(Deserialize)] 246 | pub struct GeniusStats { 247 | pub pageviews: Option, 248 | } 249 | 250 | #[derive(Deserialize)] 251 | pub struct GeniusArtistResponse { 252 | pub artist: GeniusArtist, 253 | } 254 | 255 | #[derive(Deserialize)] 256 | pub struct GeniusArtist { 257 | pub id: u32, 258 | pub name: String, 259 | pub alternate_names: Option>, 260 | pub image_url: String, 261 | pub url: String, 262 | pub description: Option, 263 | pub popular_songs: Option>, 264 | pub facebook_name: Option, 265 | pub instagram_name: Option, 266 | pub twitter_name: Option, 267 | } 268 | 269 | #[derive(Deserialize)] 270 | pub struct GeniusDescription { 271 | pub html: String, 272 | } 273 | 274 | pub struct ArtistSocial<'a> { 275 | pub name_raw: &'a str, 276 | pub name_formatted: String, 277 | pub brand: &'static str, 278 | } 279 | 280 | impl GeniusArtist { 281 | pub fn socials(&self) -> Vec { 282 | let mut socials = Vec::with_capacity(3); 283 | 284 | if let Some(name) = self.facebook_name.as_ref() { 285 | if !name.is_empty() { 286 | socials.push(ArtistSocial { 287 | name_raw: name, 288 | name_formatted: name.to_string(), 289 | brand: "facebook", 290 | }) 291 | } 292 | } 293 | 294 | if let Some(name) = self.instagram_name.as_ref() { 295 | if !name.is_empty() { 296 | socials.push(ArtistSocial { 297 | name_raw: name, 298 | name_formatted: format!("@{name}"), 299 | brand: "instagram", 300 | }) 301 | } 302 | } 303 | 304 | if let Some(name) = self.twitter_name.as_ref() { 305 | if !name.is_empty() { 306 | socials.push(ArtistSocial { 307 | name_raw: name, 308 | name_formatted: format!("@{name}"), 309 | brand: "twitter", 310 | }) 311 | } 312 | } 313 | 314 | socials 315 | } 316 | } 317 | 318 | pub enum SortMode { 319 | #[allow(dead_code)] 320 | Title, 321 | Popularity, 322 | } 323 | 324 | impl SortMode { 325 | pub fn to_query(&self) -> (&str, &str) { 326 | ( 327 | "sort", 328 | match self { 329 | Self::Title => "title", 330 | Self::Popularity => "popularity", 331 | }, 332 | ) 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/home.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, HttpRequest, Responder}; 2 | use askama::Template; 3 | 4 | use crate::{settings::settings_from_req, settings::Settings, templates::template}; 5 | 6 | #[derive(Template)] 7 | #[template(path = "home.html")] 8 | struct HomeTemplate { 9 | settings: Settings, 10 | } 11 | 12 | #[get("/")] 13 | pub async fn home(req: HttpRequest) -> impl Responder { 14 | template( 15 | &req, 16 | HomeTemplate { 17 | settings: settings_from_req(&req), 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/lyrics.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use actix_web::{get, web, HttpRequest, Responder, Result}; 4 | use askama::Template; 5 | use futures::future; 6 | 7 | use scraper::{Html, Node, Selector}; 8 | use serde::Deserialize; 9 | 10 | use crate::genius::GeniusSong; 11 | use crate::genius::{self}; 12 | use crate::settings::{settings_from_req, Settings}; 13 | use crate::templates::template; 14 | use crate::utils; 15 | 16 | static SONG_ID_SELECTOR: LazyLock = 17 | LazyLock::new(|| Selector::parse("meta[property='twitter:app:url:iphone']").unwrap()); 18 | static LYRIC_SELECTOR: LazyLock = 19 | LazyLock::new(|| Selector::parse("div[data-lyrics-container=true]").unwrap()); 20 | // The summary that used to be in the page header is now part of the lyrics container in this div 21 | static LYRIC_EXCLUDES_SELECTOR: LazyLock = 22 | LazyLock::new(|| Selector::parse("div[data-exclude-from-selection]").unwrap()); 23 | 24 | #[derive(Default)] 25 | struct Verse<'a> { 26 | title: &'a str, 27 | lyrics: Vec, 28 | } 29 | 30 | #[derive(Template)] 31 | #[template(path = "lyrics.html")] 32 | struct LyricsTemplate<'a> { 33 | settings: Settings, 34 | verses: Vec>, 35 | path: &'a str, 36 | song: GeniusSong, 37 | } 38 | 39 | #[derive(Debug, Deserialize)] 40 | pub struct LyricsQuery { 41 | id: Option, 42 | } 43 | 44 | #[get("/{path}-lyrics")] 45 | pub async fn lyrics(req: HttpRequest, info: web::Query) -> Result { 46 | let document: Html; 47 | let song: GeniusSong; 48 | 49 | // The '-lyrics' bit of the path gets cut off since we match for it explicitly, 50 | // so we need to add it back here otherwise the path will be incorrect. 51 | let path = &format!( 52 | "{}-lyrics", 53 | req.match_info().query("path").trim_end_matches('?') 54 | ); 55 | 56 | if let Some(id) = info.id { 57 | let responses = future::join( 58 | genius::get_text(genius::SubDomain::Root, path, None), 59 | genius::get_song(id), 60 | ) 61 | .await; 62 | document = Html::parse_document(&responses.0?); 63 | song = responses.1?; 64 | } else { 65 | let lyric_page = genius::get_text(genius::SubDomain::Root, path, None).await?; 66 | document = Html::parse_document(&lyric_page); 67 | let id = get_song_id(&document)?; 68 | song = genius::get_song(id).await?; 69 | } 70 | 71 | let verses = scrape_lyrics(&document); 72 | 73 | Ok(template( 74 | &req, 75 | LyricsTemplate { 76 | settings: settings_from_req(&req), 77 | verses, 78 | path, 79 | song, 80 | }, 81 | )) 82 | } 83 | 84 | fn get_song_id(document: &Html) -> crate::Result { 85 | Ok(document 86 | .select(&SONG_ID_SELECTOR) 87 | .next() 88 | .ok_or("Failed to find meta tag with song ID")? 89 | .value() 90 | .attr("content") 91 | .and_then(|content| content.strip_prefix("genius://songs/")) 92 | .ok_or("Failed to find content attribute")? 93 | .parse::()?) 94 | } 95 | 96 | fn scrape_lyrics(document: &Html) -> Vec { 97 | let mut verses = Vec::new(); 98 | let mut current_verse: Option = None; 99 | let mut new_line = false; 100 | 101 | let excluded_elements: std::collections::HashSet<_> = document 102 | .select(&LYRIC_EXCLUDES_SELECTOR) 103 | .flat_map(|e| e.descendants()) 104 | .map(|node| node.id()) 105 | .collect(); 106 | 107 | for child in document 108 | .select(&LYRIC_SELECTOR) 109 | .flat_map(|e| e.descendants()) 110 | .filter(|node| !excluded_elements.contains(&node.id())) 111 | { 112 | match child.value() { 113 | Node::Element(e) if e.name() == "br" => { 114 | new_line = true; 115 | } 116 | Node::Text(text) => { 117 | let text: &str = text; 118 | let is_title = text.starts_with('[') && text.ends_with(']'); 119 | if is_title { 120 | if let Some(curr) = current_verse { 121 | verses.push(curr); 122 | } 123 | current_verse = Some(Verse { 124 | title: text, 125 | lyrics: Vec::new(), 126 | }); 127 | } else { 128 | let curr: &mut Verse = current_verse.get_or_insert_with(Verse::default); 129 | let last = curr.lyrics.last_mut(); 130 | if new_line || last.is_none() { 131 | curr.lyrics.push(text.to_owned()); 132 | new_line = false; 133 | } else if let Some(lyric) = last { 134 | lyric.push_str(text); 135 | } 136 | } 137 | } 138 | _ => {} 139 | } 140 | } 141 | 142 | if let Some(curr) = current_verse { 143 | verses.push(curr); 144 | } else { 145 | verses.push(Verse { 146 | title: "", 147 | lyrics: vec!["This song has no lyrics.".to_owned()], 148 | }) 149 | } 150 | 151 | verses 152 | } 153 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use std::{error::Error, fs::File, io::BufReader, process::exit, time::Duration}; 4 | 5 | use actix_web::{http::StatusCode, middleware, App, HttpServer}; 6 | use clap::{arg, command, Parser}; 7 | use log::{error, info, warn, LevelFilter}; 8 | use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig}; 9 | 10 | use simplelog::{ColorChoice, CombinedLogger, Config, TermLogger, TerminalMode}; 11 | 12 | mod album; 13 | mod api; 14 | mod artist; 15 | mod errors; 16 | mod genius; 17 | mod home; 18 | mod lyrics; 19 | mod resource; 20 | mod search; 21 | mod settings; 22 | mod templates; 23 | mod utils; 24 | 25 | pub type Result = std::result::Result>; 26 | 27 | #[derive(Parser, Debug)] 28 | #[command(author, version, about, long_about = None)] 29 | struct Args { 30 | /// Sets the address to listen on 31 | #[arg(short, long, default_value = "0.0.0.0")] 32 | address: String, 33 | 34 | /// Sets the port to listen on. Will be overriden by the PORT env var if present 35 | #[arg(short, long, default_value_t = 8080)] 36 | port: u16, 37 | 38 | /// The amount of HTTP workers to use. 0 to equal physical CPU cores 39 | #[arg(short, long, default_value_t = 0)] 40 | workers: usize, 41 | 42 | /// The Keep-Alive timeout, in seconds. Set to 0 to disable. 43 | #[arg(short, long, default_value_t = 15.0)] 44 | keep_alive_timeout: f32, 45 | 46 | /// Whether TLS should be used 47 | #[arg(long, default_value = "false")] 48 | tls: bool, 49 | 50 | /// The path to the KEY file. Required when using TLS. 51 | #[arg(long, required_if_eq("tls", "true"))] 52 | tls_key_file: Option, 53 | 54 | /// The path to the CERT file. Required when using TLS. 55 | #[arg(long, required_if_eq("tls", "true"))] 56 | tls_cert_file: Option, 57 | } 58 | 59 | #[actix_web::main] 60 | async fn main() -> std::io::Result<()> { 61 | CombinedLogger::init(vec![TermLogger::new( 62 | LevelFilter::Info, 63 | Config::default(), 64 | TerminalMode::Mixed, 65 | ColorChoice::Auto, 66 | )]) 67 | .unwrap(); 68 | 69 | let args = Args::parse(); 70 | 71 | let port = std::env::var("PORT") 72 | .unwrap_or_else(|_| args.port.to_string()) 73 | .parse::() 74 | .unwrap(); 75 | 76 | info!( 77 | "Starting Intellectual v{}, listening on {}:{}!", 78 | env!("IN_VERSION"), 79 | args.address, 80 | port 81 | ); 82 | 83 | let mut server = HttpServer::new(|| { 84 | App::new() 85 | .wrap( 86 | middleware::ErrorHandlers::new() 87 | .handler(StatusCode::INTERNAL_SERVER_ERROR, errors::render_500) 88 | .handler(StatusCode::NOT_FOUND, errors::render_404) 89 | .handler(StatusCode::BAD_REQUEST, errors::render_400), 90 | ) 91 | .wrap(middleware::Compress::default()) 92 | .wrap( 93 | middleware::DefaultHeaders::new() 94 | .add(("Referrer-Policy", "no-referrer")) 95 | .add(("X-Frame-Options", "DENY")) 96 | .add(("X-Content-Type-Options", "nosniff")) 97 | .add(("Content-Security-Policy", "default-src 'self'")), 98 | ) 99 | // Routes 100 | .service(album::album) 101 | .service(api::image) 102 | .service(artist::artist) 103 | .service(home::home) 104 | .service(lyrics::lyrics) 105 | .service(search::search) 106 | .service(settings::settings) 107 | .service(settings::settings_form) 108 | // Static Resources 109 | .service(resource::resource) 110 | }); 111 | 112 | if args.keep_alive_timeout > 0.0 { 113 | server = server.keep_alive(Duration::from_secs_f32(args.keep_alive_timeout)); 114 | } else { 115 | server = server.keep_alive(None) 116 | } 117 | 118 | if args.workers > 0 { 119 | server = server.workers(args.workers); 120 | } 121 | 122 | if args.tls { 123 | // To create a self-signed temporary cert for testing: 124 | // openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost' 125 | server.bind_rustls_021((args.address.to_owned(), port), build_tls_config(&args)?) 126 | } else { 127 | server.bind_auto_h2c((args.address, port)) 128 | }? 129 | .run() 130 | .await 131 | } 132 | 133 | fn build_tls_config(args: &Args) -> std::io::Result { 134 | Ok(RustlsServerConfig::builder() 135 | .with_safe_defaults() 136 | .with_no_client_auth() 137 | .with_single_cert(create_cert_chain(args), PrivateKey(create_key(args))) 138 | .unwrap()) 139 | } 140 | 141 | fn create_cert_chain(args: &Args) -> Vec { 142 | let cert_file_path = args.tls_cert_file.as_ref().unwrap(); 143 | let cert_file = &mut BufReader::new(match File::open(cert_file_path) { 144 | Ok(file) => file, 145 | Err(err) => { 146 | error!("Failed to load cert file '{}': {}", cert_file_path, err); 147 | exit(1); 148 | } 149 | }); 150 | 151 | let cert_chain: Vec = rustls_pemfile::certs(cert_file) 152 | .unwrap() 153 | .into_iter() 154 | .map(Certificate) 155 | .collect(); 156 | if cert_chain.is_empty() { 157 | error!("Failed to find any certs in '{}'", cert_file_path); 158 | exit(1); 159 | } 160 | cert_chain 161 | } 162 | 163 | fn create_key(args: &Args) -> Vec { 164 | let key_file_path = args.tls_key_file.as_ref().unwrap(); 165 | let key_file = &mut BufReader::new(match File::open(key_file_path) { 166 | Ok(file) => file, 167 | Err(err) => { 168 | error!("Failed to load key file '{}': {}", key_file_path, err); 169 | exit(1); 170 | } 171 | }); 172 | let mut keys: Vec> = rustls_pemfile::pkcs8_private_keys(key_file).unwrap(); 173 | if keys.is_empty() { 174 | error!("Failed to find any keys in '{}'", key_file_path); 175 | exit(1); 176 | } 177 | if keys.len() > 1 { 178 | warn!( 179 | "Found multiple keys in '{}'! Only the first will be used.", 180 | key_file_path 181 | ); 182 | } 183 | 184 | keys.remove(0) 185 | } 186 | -------------------------------------------------------------------------------- /src/resource.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, HttpResponse, Responder}; 2 | use include_dir::{include_dir, Dir}; 3 | 4 | const STATIC_RESOURCES: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); 5 | 6 | #[get("{filename:.*}")] 7 | pub async fn resource(path: web::Path) -> impl Responder { 8 | asset(path.as_str()) 9 | } 10 | 11 | fn asset(path: &str) -> impl Responder { 12 | let file = match STATIC_RESOURCES.get_file(path) { 13 | Some(file) => file, 14 | None => return HttpResponse::NotFound().finish(), 15 | }; 16 | HttpResponse::Ok() 17 | .append_header(("Content-Type", content_type(path))) 18 | .append_header(("Cache-Control", "public, max-age=31536000, immutable")) 19 | .body(file.contents()) 20 | } 21 | 22 | fn content_type(path: &str) -> &str { 23 | match path.split('.').next_back().unwrap_or_default() { 24 | "css" => "text/css", 25 | "svg" => "image/svg+xml", 26 | "woff2" => "font/woff2", 27 | "json" => "application/json", 28 | _ => "text/plain", 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{max, min}; 2 | use std::ops::RangeInclusive; 3 | 4 | use actix_web::{get, web, HttpRequest, Responder, Result}; 5 | use askama::Template; 6 | use serde::Deserialize; 7 | 8 | use crate::genius::{self, GeniusSong}; 9 | use crate::settings::{settings_from_req, Settings}; 10 | use crate::templates::template; 11 | use crate::utils; 12 | 13 | const NAV_PAGE_COUNT: u8 = 3; 14 | 15 | #[derive(Template)] 16 | #[template(path = "search.html")] 17 | struct SearchTemplate { 18 | settings: Settings, 19 | songs: Vec, 20 | q: String, 21 | current_page: u8, 22 | nav_pages: Vec, 23 | } 24 | 25 | #[derive(Debug, Deserialize)] 26 | pub struct SearchQuery { 27 | q: String, 28 | page: Option, 29 | } 30 | 31 | #[get("/search")] 32 | pub async fn search(req: HttpRequest, info: web::Query) -> Result { 33 | let current_page = info.page.unwrap_or(1); 34 | 35 | let songs = genius::get_search_results(&info.q, current_page).await?; 36 | 37 | let nav_min = max(1, current_page.saturating_sub(NAV_PAGE_COUNT)); 38 | let nav_max = min(100, current_page.saturating_add(NAV_PAGE_COUNT)); 39 | let nav_pages = RangeInclusive::new(nav_min, nav_max).collect(); 40 | 41 | Ok(template( 42 | &req, 43 | SearchTemplate { 44 | settings: settings_from_req(&req), 45 | q: info.q.to_owned(), 46 | current_page, 47 | nav_pages, 48 | songs, 49 | }, 50 | )) 51 | } 52 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use actix_web::{get, post, web::Form, HttpRequest, HttpResponse, Responder}; 4 | use askama::Template; 5 | use cookie::Cookie; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::templates::template; 9 | 10 | pub const SETTINGS_KEY: &str = "settings"; 11 | 12 | static THEME_CONFIG: LazyLock = LazyLock::new(|| { 13 | serde_json::from_str(include_str!("../static/style/theme/themes.json")).unwrap() 14 | }); 15 | 16 | #[derive(Template)] 17 | #[template(path = "settings.html")] 18 | struct SettingsTemplate { 19 | settings: Settings, 20 | themes: Vec, 21 | } 22 | 23 | #[get("/settings")] 24 | pub async fn settings(req: HttpRequest) -> impl Responder { 25 | template( 26 | &req, 27 | SettingsTemplate { 28 | settings: settings_from_req(&req), 29 | themes: THEME_CONFIG.themes.clone(), 30 | }, 31 | ) 32 | } 33 | 34 | #[post("/settings")] 35 | pub async fn settings_form(form: Form) -> impl Responder { 36 | match serde_json::to_string(&form) { 37 | Ok(str) => HttpResponse::SeeOther() 38 | .cookie(Cookie::new(SETTINGS_KEY, str)) 39 | .append_header(("Location", "/settings")) 40 | .finish(), 41 | Err(_) => HttpResponse::BadRequest().finish(), 42 | } 43 | } 44 | 45 | #[derive(Serialize, Deserialize)] 46 | pub struct Settings { 47 | pub theme: String, 48 | } 49 | 50 | impl Default for Settings { 51 | fn default() -> Self { 52 | Settings { 53 | theme: "github-dark".into(), 54 | } 55 | } 56 | } 57 | 58 | impl Settings { 59 | pub fn is_valid(&self) -> bool { 60 | THEME_CONFIG.themes.iter().any(|t| t.id == self.theme) 61 | } 62 | } 63 | 64 | pub fn settings_from_req(req: &HttpRequest) -> Settings { 65 | req.cookie(SETTINGS_KEY) 66 | .and_then(|cookie| serde_json::from_str::(cookie.value()).ok()) 67 | .filter(|s| s.is_valid()) 68 | .unwrap_or_default() 69 | } 70 | 71 | #[derive(Clone, Deserialize)] 72 | struct Theme { 73 | id: String, 74 | name: String, 75 | } 76 | 77 | #[derive(Deserialize)] 78 | struct ThemeConfig { 79 | themes: Vec, 80 | } 81 | -------------------------------------------------------------------------------- /src/templates.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{HttpRequest, HttpResponse, HttpResponseBuilder}; 2 | use askama::Template; 3 | 4 | use crate::settings::SETTINGS_KEY; 5 | 6 | pub fn template(req: &HttpRequest, t: impl Template) -> HttpResponse { 7 | template_with_res(req, HttpResponse::Ok(), t) 8 | } 9 | 10 | pub fn template_with_res( 11 | req: &HttpRequest, 12 | mut res: HttpResponseBuilder, 13 | t: impl Template, 14 | ) -> HttpResponse { 15 | res.append_header(("Content-Type", "text/html; charset=utf-8")) 16 | // Caching Setup 17 | // Since Cloudflare ignores Vary headers, we can't publically cache all pages since only 18 | // the last-cached theme would be shown to users. Instead, we privately cache all pages in the 19 | // browser, which does handle the Vary header correctly. If we didn't have the Vary header, 20 | // when a user changes themes, it won't be applied to previously visited pages (e.g. the 21 | // home page) until the browser requests the page from the server again. 22 | .append_header(("Vary", "Cookie")) 23 | .append_header(("Cache-Control", "private, max-age=604800")); 24 | 25 | // To allow the browser to properly discard old pages, we must send back the settings cookie. 26 | if let Some(cookie) = req.cookie(SETTINGS_KEY) { 27 | res.cookie(cookie); 28 | } 29 | 30 | res.body(t.render().unwrap_or_default()) 31 | } 32 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn pretty_format_num(num: i32) -> String { 2 | if num >= 1_000_000 { 3 | format!("{:.1}M", num as f32 / 1_000_000.0) 4 | } else if num >= 1_000 { 5 | format!("{}K", num / 1_000) 6 | } else { 7 | format!("{num}") 8 | } 9 | } 10 | 11 | pub fn borrowed_u8_eq(a: &u8, b: &u8) -> bool { 12 | *a == *b 13 | } 14 | 15 | /// Gets the path part from a URL. Will panic if the URL doesn't have any '/'. 16 | pub fn path_from_url(url: &str) -> String { 17 | url.splitn(4, '/').last().unwrap().to_owned() 18 | } 19 | -------------------------------------------------------------------------------- /static/font/InterVariable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Insprill/intellectual/8d1b5b1bb3c99881ebf059e96e48e50e965f0d5e/static/font/InterVariable.woff2 -------------------------------------------------------------------------------- /static/font/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION AND CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /static/font/inter.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:InterVariable;font-style:normal;font-weight:100 900;font-display:swap;src:url("InterVariable.woff2")format("woff2");} 2 | -------------------------------------------------------------------------------- /static/icon/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icon/instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icon/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Intellectual", 3 | "short_name": "Intellectual", 4 | "display": "standalone", 5 | "background_color": "#0d1117", 6 | "description": "Alternate frontend for Genius focused on privacy and simplicity", 7 | "theme_color": "#0d1117" 8 | } 9 | -------------------------------------------------------------------------------- /static/style/artist.css: -------------------------------------------------------------------------------- 1 | .artist { 2 | display: grid; 3 | grid-template-columns: 15em 25em; 4 | grid-template-rows: 6.5em 8.5em 1fr; 5 | gap: 1em 1em; 6 | grid-template-areas: 7 | "image name" 8 | "image social" 9 | "description description" 10 | "songs songs"; 11 | padding: 0.75rem; 12 | margin: 2rem auto; 13 | border-radius: var(--radius); 14 | background: var(--foreground); 15 | color: var(--text); 16 | } 17 | 18 | .artist-image { 19 | grid-area: image; 20 | object-fit: cover; 21 | width: 100%; 22 | max-height: 100%; 23 | border-radius: var(--radius); 24 | } 25 | 26 | @media only screen and (min-width: 513px) { 27 | .artist-image { 28 | height: 100%; 29 | } 30 | } 31 | 32 | .artist-info { 33 | grid-area: name; 34 | display: grid; 35 | grid-template-columns: 1fr; 36 | grid-template-rows: auto; 37 | gap: 0 0; 38 | grid-auto-flow: row; 39 | grid-template-areas: 40 | "artist-name" 41 | "artist-nicknames"; 42 | } 43 | 44 | .artist-name { 45 | grid-area: artist-name; 46 | font-weight: 900; 47 | font-size: 36px; 48 | margin: 0; 49 | } 50 | 51 | .artist-nicknames { 52 | grid-area: artist-nicknames; 53 | font-weight: 300; 54 | font-size: 14px; 55 | margin: 0; 56 | } 57 | 58 | .artist-description { 59 | grid-area: description; 60 | } 61 | 62 | .artist-songs { 63 | grid-area: songs; 64 | } 65 | 66 | .artist-search-songs { 67 | padding: 10px 20px; 68 | border-radius: var(--radius); 69 | background-color: var(--background); 70 | color: var(--text); 71 | border: none; 72 | cursor: pointer; 73 | font-size: 16px; 74 | font-weight: 600; 75 | text-decoration: none; 76 | } 77 | 78 | .artist-search-songs:hover { 79 | background: var(--highlighted); 80 | transform: var(--hover-transform); 81 | } 82 | 83 | .artist-socials { 84 | grid-area: social; 85 | display: flex; 86 | flex-direction: column; 87 | gap: 0.5em 0; 88 | } 89 | 90 | .social { 91 | padding: 0.4em; 92 | border-radius: var(--radius); 93 | text-decoration: none; 94 | display: grid; 95 | grid-template-columns: 2em 1fr; 96 | grid-template-rows: 1fr; 97 | gap: 5px 0; 98 | grid-template-areas: 99 | "icon name"; 100 | align-items: center; 101 | transition: var(--hover-transition); 102 | } 103 | 104 | .social:hover { 105 | box-shadow: var(--shadow); 106 | transform: var(--hover-transform); 107 | } 108 | 109 | .social-icon { 110 | grid-area: icon; 111 | width: 26px; 112 | height: 26px; 113 | } 114 | 115 | .social-name { 116 | grid-area: name; 117 | color: var(--text); 118 | font-size: 22px; 119 | font-weight: 400; 120 | margin: 0; 121 | } 122 | 123 | .facebook { 124 | grid-area: facebook; 125 | background-color: var(--facebook); 126 | } 127 | 128 | .instagram { 129 | grid-area: instagram; 130 | background-color: var(--instagram); 131 | } 132 | 133 | .twitter { 134 | grid-area: twitter; 135 | background-color: var(--twitter); 136 | } 137 | 138 | @media only screen and (max-width: 512px) { 139 | .artist { 140 | grid-template-columns: auto; 141 | grid-template-rows: auto; 142 | grid-template-areas: 143 | "image" 144 | "name" 145 | "social" 146 | "description" 147 | "songs"; 148 | } 149 | } 150 | 151 | @media only screen and (min-width: 513px) and (max-width: 768px) { 152 | .artist { 153 | grid-template-columns: auto auto auto; 154 | grid-template-rows: auto; 155 | grid-template-areas: 156 | "image name" 157 | "image social" 158 | "description description" 159 | "songs songs"; 160 | } 161 | } 162 | 163 | 164 | 165 | /* Genius Styling */ 166 | 167 | .artist-description img { 168 | object-fit: scale-down; 169 | width: 100%; 170 | height: 100%; 171 | } 172 | 173 | .embedly_preview a { 174 | text-decoration: none; 175 | } 176 | 177 | .gray_container { 178 | display: flex; 179 | border-radius: var(--radius); 180 | box-shadow: var(--shadow); 181 | transition: var(--hover-transition); 182 | } 183 | 184 | .gray_container:hover { 185 | background: var(--highlighted); 186 | transform: var(--hover-transform); 187 | } 188 | 189 | .gray_container div { 190 | display: flex; 191 | margin: 10px auto 10px auto; 192 | } 193 | 194 | .embedly_preview-text { 195 | flex-direction: column; 196 | gap: 10px; 197 | } 198 | 199 | .embedly_preview-title { 200 | font-weight: 900; 201 | } 202 | 203 | .embedly_preview-dash, .embedly_preview-provider { 204 | display: none; 205 | } 206 | 207 | .embedly_preview-thumb img { 208 | object-fit: cover; 209 | overflow: hidden; 210 | margin: 10px; 211 | margin-top: auto; 212 | border-radius: var(--radius); 213 | } 214 | -------------------------------------------------------------------------------- /static/style/error.css: -------------------------------------------------------------------------------- 1 | .card { 2 | color: var(--text); 3 | background: var(--foreground); 4 | border-radius: var(--radius); 5 | box-shadow: var(--shadow); 6 | text-decoration: none; 7 | justify-content: center; 8 | flex-direction: column; 9 | padding: 10px 20px; 10 | margin: auto; 11 | border-radius: var(--radius); 12 | } 13 | -------------------------------------------------------------------------------- /static/style/home.css: -------------------------------------------------------------------------------- 1 | .panel-container { 2 | margin: auto; 3 | font-size: 130%; 4 | } 5 | 6 | .search-bar > form { 7 | padding: 12px; 8 | border-radius: var(--radius); 9 | display: flex; 10 | box-shadow: var(--shadow); 11 | margin: -50px auto auto; 12 | background: var(--foreground); 13 | } 14 | 15 | .search-bar > form button { 16 | background: var(--background); 17 | color: var(--text); 18 | border: 0; 19 | border-radius: var(--radius); 20 | cursor: pointer; 21 | font-weight: bold; 22 | font-size: 18px; 23 | } 24 | 25 | .search-bar > form input { 26 | background: var(--background); 27 | color: var(--text); 28 | font-size: 24px; 29 | width: 100%; 30 | border: 0; 31 | border-radius: var(--radius); 32 | padding: 4px; 33 | margin-right: 8px; 34 | height: unset; 35 | } 36 | -------------------------------------------------------------------------------- /static/style/lyrics.css: -------------------------------------------------------------------------------- 1 | .song-lyrics { 2 | padding: 0.75rem; 3 | margin: 2rem auto; 4 | border-radius: var(--radius); 5 | background: var(--foreground); 6 | color: var(--text); 7 | } 8 | 9 | .song-lyric { 10 | margin: 0.33rem; 11 | } 12 | 13 | .header { 14 | display: grid; 15 | grid-template-columns: 12em auto; 16 | grid-template-rows: 3em 3em 6em; 17 | gap: 0 0.75em; 18 | grid-template-areas: 19 | "cover song" 20 | "cover song" 21 | "cover stats"; 22 | } 23 | 24 | @media only screen and (max-width: 512px) { 25 | .header { 26 | grid-template-columns: auto; 27 | grid-template-rows: auto; 28 | grid-template-areas: 29 | "cover" 30 | "song" 31 | "stats"; 32 | } 33 | } 34 | 35 | .header-cover { 36 | grid-area: cover; 37 | width: 100%; 38 | height: 100%; 39 | border-radius: var(--radius); 40 | } 41 | 42 | .song-info { 43 | display: grid; 44 | grid-auto-columns: 1fr; 45 | grid-template-columns: auto; 46 | grid-template-rows: auto; 47 | gap: 0.25em 0; 48 | grid-template-areas: 49 | "title" 50 | "artist" 51 | "album"; 52 | grid-area: song; 53 | margin-top: 1em; 54 | } 55 | 56 | .title { 57 | grid-area: title; 58 | font-weight: 900; 59 | font-size: min(6vw, 32px); 60 | margin: 0; 61 | } 62 | 63 | @media only screen and (min-width: 513px) and (max-width: 768px) { 64 | .title { 65 | font-size: min(4vw, 32px); 66 | } 67 | } 68 | 69 | .artist-name { 70 | color: var(--text); 71 | grid-area: artist; 72 | font-weight: 500; 73 | font-size: 18px; 74 | margin: 0; 75 | } 76 | 77 | .album-name { 78 | color: var(--text); 79 | grid-area: album; 80 | font-weight: 500; 81 | font-size: 18px; 82 | margin: 0; 83 | } 84 | 85 | .stats { 86 | display: grid; 87 | grid-template-columns: 1fr 1fr; 88 | grid-template-rows: 1fr; 89 | gap: 0 1.5em; 90 | grid-template-areas: 91 | "release-date views"; 92 | grid-area: stats; 93 | margin-top: auto; 94 | } 95 | 96 | .stats-release-date { 97 | grid-area: release-date; 98 | font-size: 14px; 99 | } 100 | 101 | .stats-views { 102 | grid-area: views; 103 | font-size: 14px; 104 | text-align: right; 105 | } 106 | -------------------------------------------------------------------------------- /static/style/search.css: -------------------------------------------------------------------------------- 1 | .pagination { 2 | display: inline-block; 3 | margin: auto; 4 | } 5 | 6 | .pagination a { 7 | float: left; 8 | color: var(--text); 9 | background-color: var(--background); 10 | padding: 8px 16px; 11 | text-decoration: none; 12 | border-radius: var(--radius); 13 | transition: var(--hover-transition); 14 | } 15 | 16 | .pagination a.active { 17 | transform: var(--hover-transform); 18 | background-color: var(--highlighted); 19 | } 20 | 21 | .pagination a:hover:not(.active) { 22 | transform: var(--hover-transform); 23 | background-color: var(--foreground); 24 | } 25 | -------------------------------------------------------------------------------- /static/style/settings.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | margin: auto; 3 | } 4 | 5 | .settings > form { 6 | display: grid; 7 | grid-gap: 12px 8px; 8 | grid-template-columns: auto; 9 | grid-template-areas: 10 | "theme-title theme" 11 | "save save"; 12 | padding: 12px; 13 | border-radius: var(--radius); 14 | box-shadow: var(--shadow); 15 | margin: -50px auto auto; 16 | background: var(--foreground); 17 | } 18 | 19 | .settings > form label { 20 | color: var(--text); 21 | font-size: 16px; 22 | } 23 | 24 | .settings > form option { 25 | background: var(--background); 26 | color: var(--text); 27 | } 28 | 29 | .settings > form button { 30 | background: var(--background); 31 | color: var(--text); 32 | border: 0; 33 | border-radius: var(--radius); 34 | cursor: pointer; 35 | font-weight: bold; 36 | font-size: 18px; 37 | } 38 | 39 | .settings > form input { 40 | background: var(--background); 41 | color: var(--text); 42 | font-size: 24px; 43 | width: 100%; 44 | border: 0; 45 | border-radius: var(--radius); 46 | padding: 4px; 47 | margin-right: 8px; 48 | height: unset; 49 | } 50 | 51 | .theme { 52 | grid-area: theme; 53 | } 54 | 55 | .theme-title { 56 | grid-area: theme-title; 57 | } 58 | 59 | .save { 60 | grid-area: save; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /static/style/song.css: -------------------------------------------------------------------------------- 1 | .song-list { 2 | display: flex; 3 | justify-content: center; 4 | flex-direction: column; 5 | max-width: 1000px; 6 | padding: 10px 20px; 7 | margin: 0 auto; 8 | border-radius: var(--radius); 9 | } 10 | 11 | .song { 12 | background: var(--foreground); 13 | border-radius: var(--radius); 14 | box-shadow: var(--shadow); 15 | transition: var(--hover-transition); 16 | text-decoration: none; 17 | display: grid; 18 | padding: 10px; 19 | grid-template-columns: 9em 1fr; 20 | grid-template-rows: 3em 4em 2em; 21 | grid-column-gap: 10px; 22 | grid-template-areas: 23 | "thumbnail title ." 24 | "thumbnail artist ." 25 | "thumbnail views ."; 26 | } 27 | 28 | .song:hover { 29 | background: var(--highlighted); 30 | transform: var(--hover-transform); 31 | } 32 | 33 | .song:not(:last-child) { 34 | margin-bottom: 10px; 35 | } 36 | 37 | .song-title { 38 | grid-area: title; 39 | font-weight: 900; 40 | color: var(--text); 41 | font-size: min(4vw, 32px); 42 | text-overflow: ellipsis; 43 | overflow: hidden; 44 | height: 1.2em; 45 | white-space: nowrap; 46 | } 47 | 48 | .song-artist { 49 | grid-area: artist; 50 | font-weight: 600; 51 | font-size: 16px; 52 | color: var(--text); 53 | } 54 | 55 | .song-views { 56 | grid-area: views; 57 | font-weight: 500; 58 | font-size: 12px; 59 | color: var(--text); 60 | } 61 | 62 | .song-thumbnail { 63 | grid-area: thumbnail; 64 | width: 100%; 65 | height: 100%; 66 | border-radius: var(--radius); 67 | } 68 | -------------------------------------------------------------------------------- /static/style/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --facebook: #4267b2; 3 | --instagram: #e1306c; 4 | --twitter: #1da1f2; 5 | --radius: 5px; 6 | --shadow: 0 0 15px rgba(0, 0, 0, 0.3); 7 | --hover-transform: translate3d(0px, -1px, 0px); 8 | --hover-transition: 0.3s all; 9 | } 10 | 11 | a { 12 | color: var(--text); 13 | } 14 | 15 | /* region Navigation Bar */ 16 | 17 | nav { 18 | display: flex; 19 | align-items: center; 20 | position: fixed; 21 | background-color: var(--foreground); 22 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); 23 | width: 100%; 24 | height: 50px; 25 | z-index: 1000; 26 | } 27 | 28 | .inner-nav { 29 | margin: auto; 30 | box-sizing: border-box; 31 | padding: 0 10px; 32 | display: flex; 33 | align-items: center; 34 | flex-basis: 920px; 35 | height: 50px; 36 | } 37 | 38 | .nav-item { 39 | display: flex; 40 | flex: 1; 41 | line-height: 50px; 42 | height: 50px; 43 | overflow: hidden; 44 | flex-wrap: wrap; 45 | align-items: center; 46 | } 47 | 48 | .nav-item.right { 49 | text-align: right; 50 | justify-content: flex-end; 51 | } 52 | 53 | .nav-item.right a { 54 | padding-left: 4px; 55 | text-decoration: unset; 56 | } 57 | 58 | .site-name { 59 | font-size: 26px; 60 | font-weight: 800; 61 | line-height: 1; 62 | color: var(--yellow); 63 | text-decoration: unset; 64 | } 65 | 66 | .site-version { 67 | font-size: 12px; 68 | font-weight: 600; 69 | line-height: 1; 70 | color: var(--text-dark); 71 | text-decoration: unset; 72 | } 73 | 74 | /* endregion */ 75 | 76 | .container { 77 | display: flex; 78 | flex-wrap: wrap; 79 | box-sizing: border-box; 80 | padding-top: 50px; 81 | margin: auto; 82 | min-height: 100vh; 83 | } 84 | 85 | html, body { 86 | margin: 0; 87 | padding: 0; 88 | font-family: Inter var, sans-serif; 89 | background-color: var(--background); 90 | } 91 | 92 | .external-link { 93 | font-weight: 500; 94 | color: var(--text); 95 | } 96 | 97 | .text-centered { 98 | text-align: center; 99 | } 100 | -------------------------------------------------------------------------------- /static/style/theme/catppuccin-frappe.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/catppuccin/catppuccin#-palette */ 2 | :root { 3 | color-scheme: dark; 4 | --yellow: #e5c890; /* Yellow */ 5 | --text: #c6d0f5; /* Text */ 6 | --text-dark: #838ba7; /* Overlay 1 */ 7 | --foreground: #303446; /* Base */ 8 | --background: #292c3c; /* Mantle */ 9 | --highlighted: #414559; /* Surface 0 */ 10 | } 11 | -------------------------------------------------------------------------------- /static/style/theme/catppuccin-latte.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/catppuccin/catppuccin#-palette */ 2 | :root { 3 | color-scheme: light; 4 | --yellow: #df8e1d; /* Yellow */ 5 | --text: #4c4f69; /* Text */ 6 | --text-dark: #8c8fa1; /* Overlay 1 */ 7 | --foreground: #eff1f5; /* Base */ 8 | --background: #e6e9ef; /* Mantle */ 9 | --highlighted: #ccd0da; /* Surface 0 */ 10 | } 11 | -------------------------------------------------------------------------------- /static/style/theme/catppuccin-macchiato.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/catppuccin/catppuccin#-palette */ 2 | :root { 3 | color-scheme: dark; 4 | --yellow: #eed49f; /* Yellow */ 5 | --text: #cad3f5; /* Text */ 6 | --text-dark: #8087a2; /* Overlay 1 */ 7 | --foreground: #24273a; /* Base */ 8 | --background: #1e2030; /* Mantle */ 9 | --highlighted: #363a4f; /* Surface 0 */ 10 | } 11 | -------------------------------------------------------------------------------- /static/style/theme/catppuccin-mocha.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/catppuccin/catppuccin#-palette */ 2 | :root { 3 | color-scheme: dark; 4 | --yellow: #f9e2af; /* Yellow */ 5 | --text: #cdd6f4; /* Text */ 6 | --text-dark: #7f849c; /* Overlay 1 */ 7 | --foreground: #1e1e2e; /* Base */ 8 | --background: #181825; /* Mantle */ 9 | --highlighted: #313244; /* Surface 0 */ 10 | } 11 | -------------------------------------------------------------------------------- /static/style/theme/github-dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: dark; 3 | --yellow: #ffff64; 4 | --text: #c9d1d9; 5 | --text-dark: #85817b; 6 | --foreground: #161b22; 7 | --background: #0d1117; 8 | --highlighted: #1b2027; 9 | } 10 | -------------------------------------------------------------------------------- /static/style/theme/github-light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: light; 3 | --yellow: #d7d741; 4 | --text: #24292f; 5 | --text-dark: #85817b; 6 | --foreground: #f6f8fa; 7 | --background: #ffffff; 8 | --highlighted: #f0f2f4; 9 | } 10 | -------------------------------------------------------------------------------- /static/style/theme/gruvbox-dark.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/morhetz/gruvbox?tab=readme-ov-file#dark-mode-1 */ 2 | :root { 3 | color-scheme: dark; 4 | --yellow: #fabd2f; /* yellow */ 5 | --text: #ebdbb2; /* fg */ 6 | --text-dark: #665c53; /* bg3 */ 7 | --foreground: #3c3836; /* bg1 */ 8 | --background: #282828; /* bg0 */ 9 | --highlighted: #504945; /* bg2 */ 10 | } 11 | -------------------------------------------------------------------------------- /static/style/theme/gruvbox-light.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/morhetz/gruvbox?tab=readme-ov-file#light-mode-1 */ 2 | :root { 3 | color-scheme: light; 4 | --yellow: #d79921; /* yellow */ 5 | --text: #3c3836; /* fg */ 6 | --text-dark: #bdae93; /* bg3 */ 7 | --foreground: #ebdbb2; /* bg1 */ 8 | --background: #fbf1c7; /* bg0 */ 9 | --highlighted: #d5c4a1; /* bg2 */ 10 | } 11 | -------------------------------------------------------------------------------- /static/style/theme/sweet.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/EliverLara/Sweet/blob/master/gtk-4.0/_colors.scss */ 2 | :root { 3 | color-scheme: dark; 4 | --yellow: #f9dc5c; 5 | --text: #c9d1d9; 6 | --text-dark: #85817b; 7 | --foreground: #191925; 8 | --background: #1e1e2e; 9 | --highlighted: #1b1b2a; 10 | --shadow: 0 0 15px rgba(162, 162, 165, 0.2); 11 | } 12 | -------------------------------------------------------------------------------- /static/style/theme/themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "themes": [ 3 | { 4 | "name": "Catppuccin Latte", 5 | "id": "catppuccin-latte" 6 | }, 7 | { 8 | "name": "Catppuccin Frappé", 9 | "id": "catppuccin-frappe" 10 | }, 11 | { 12 | "name": "Catppuccin Macchiato", 13 | "id": "catppuccin-macchiato" 14 | }, 15 | { 16 | "name": "Catppuccin Mocha", 17 | "id": "catppuccin-mocha" 18 | }, 19 | { 20 | "name": "Github Dark", 21 | "id": "github-dark" 22 | }, 23 | { 24 | "name": "Github Light", 25 | "id": "github-light" 26 | }, 27 | { 28 | "name": "Gruvbox Dark", 29 | "id": "gruvbox-dark" 30 | }, 31 | { 32 | "name": "Gruvbox Light", 33 | "id": "gruvbox-light" 34 | }, 35 | { 36 | "name": "Sweet", 37 | "id": "sweet" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /templates/400.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Bad Request - {% endblock %} 4 | 5 | {% block style %}/style/error.css{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Bad Request

10 | {% if err.is_some() %} 11 |

{{ err.as_ref().unwrap() }}

12 | {% endif %} 13 |
14 |

If this continues to occur, please report it.

15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page Not Found - {% endblock %} 4 | 5 | {% block style %}/style/error.css{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Page Not Found

10 |

The page you requested couldn't be found!

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Internal Error - {% endblock %} 4 | 5 | {% block style %}/style/error.css{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Internal Error

10 | {% if err.is_some() %} 11 |

{{ err.as_ref().unwrap() }}

12 | {% endif %} 13 |
14 |

If this continues to occur, please report it.

15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/album.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{album.name}} - {% endblock %} 4 | 5 | {% block style %}/style/lyrics.css{% endblock %} 6 | {% block head %} 7 | 8 | {% endblock %} 9 | 10 | {% block navright %} 11 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 |
18 |
19 |
20 |

{{ album.name|e }}

21 |

By 22 | 23 | {{ album.artist.name|e }} 24 | 25 |

26 |
27 |
28 | {% if album.release_date_for_display.is_some() %} 29 |

Released on {{ album.release_date_for_display.as_ref().unwrap()|e }}

30 | {% endif %} 31 |
32 | Thumbnail 33 |
34 |
35 | {% if album.tracks.is_some() %} 36 |
37 |

Tracks

38 | {% for song in album.tracks.as_ref().unwrap() %} 39 | {% include "song.html" %} 40 | {% endfor %} 41 |
42 | {% endif %} 43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /templates/artist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{artist.name}} - {% endblock %} 4 | 5 | {% block style %}/style/artist.css{% endblock %} 6 | {% block head %} 7 | 8 | {% endblock %} 9 | 10 | 11 | {% block navright %} 12 | 15 | {% endblock %} 16 | 17 | {% block content %} 18 |
19 | Thumbnail 20 |
21 |

{{ artist.name|e }}

22 | {% if artist.alternate_names.is_some() && !artist.alternate_names.as_ref().unwrap().is_empty() %} 23 |

24 | AKA: 25 | {% for nickname in artist.alternate_names.as_ref().unwrap() %} 26 | {{ nickname|e }}{% if !loop.last %},{% endif %} 27 | {% endfor %} 28 |

29 | {% endif %} 30 |
31 |
32 | {% for social in artist.socials() %} 33 | 37 | {% endfor %} 38 |
39 | {% if artist.description.is_some() %} 40 |
{{ artist.description.as_ref().unwrap().html|safe|paragraphbreaks|safe }}
41 | {% endif %} 42 | 43 | {% if artist.popular_songs.is_some() %} 44 |
45 |

Popular Songs

46 | {% for song in artist.popular_songs.as_ref().unwrap() %} 47 | {% include "song.html" %} 48 | {% endfor %} 49 | Search for songs 50 |
51 | {% endif %} 52 |
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %}Intellectual 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block head %} 15 | {% endblock %} 16 | 17 | 18 | 19 | 30 | 31 |
32 | {% block content %} 33 | {% endblock %} 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block style %}/style/home.css{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block navright %} 10 | Settings 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/lyrics.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{song.title}} - {% endblock %} 4 | 5 | {% block style %}/style/lyrics.css{% endblock %} 6 | 7 | {% block navright %} 8 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{{ song.title|e }}

18 |

By 19 | 20 | {{ song.primary_artist.name|e }} 21 | 22 |

23 | {% if song.album.is_some() %} 24 |

On 25 | 26 | {{ song.album.as_ref().unwrap().name|e }} 27 | 28 |

29 | {% endif %} 30 |
31 |
32 | {% if song.release_date_for_display.is_some() %} 33 |

Released on {{ song.release_date_for_display.as_ref().unwrap()|e }}

34 | {% endif %} 35 | {% if song.stats.pageviews.is_some() %} 36 |

{{ utils::pretty_format_num(song.stats.pageviews.unwrap())|e }} Views

37 | {% endif %} 38 |
39 | Thumbnail 40 |
41 |
42 | {% for verse in verses %} 43 |

{{ verse.title|e }}

44 | {% for lyric in verse.lyrics %} 45 |

{{ lyric|e }}

46 | {% endfor %} 47 | {% if !loop.last %} 48 |
49 | {% endif %} 50 | {% endfor %} 51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Search - {% endblock %} 4 | 5 | {% block style %}/style/search.css{% endblock %} 6 | {% block head %} 7 | 8 | {% endblock %} 9 | 10 | {% block navright %} 11 | Search on Genius 12 | {% endblock %} 13 | 14 | {% block content %} 15 |
16 | {% for song in songs %} 17 | {% include "song.html" %} 18 | {% endfor %} 19 | 36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Settings - {% endblock %} 4 | 5 | {% block style %}/style/settings.css{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 | 16 | 19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/song.html: -------------------------------------------------------------------------------- 1 | 2 | Thumbnail 6 |

{{ song.title|e }}

7 |

{{ song.primary_artist.name|e }}

8 | {% if song.stats.pageviews.is_some() %} 9 |

{{ utils::pretty_format_num(song.stats.pageviews.unwrap())|e }} Views

10 | {% endif %} 11 |
12 | --------------------------------------------------------------------------------