37 Commits

Author SHA1 Message Date
37b639383f fix(deps): update dependency cheerio to v1.1.0
Some checks failed
renovate/artifacts Artifact file update failure
2025-06-10 17:02:10 +00:00
ngn
f717a6b610 ups: update to dded1e5
Some checks failed
docker / docker (push) Successful in 1m33s
ups / ups (push) Failing after 39s
2025-06-10 09:48:19 +03:00
648d5cde32 fix(media): fix videos not playing
fixes mimetype
also adds other playable trailers, if they exist
2025-06-10 09:48:05 +03:00
288ec34fb4 feat: redirect paths from specific languages 2025-06-10 09:48:00 +03:00
4e03757d69 chore(build): add sharp to trusted dep 2025-06-10 09:47:54 +03:00
f92035ed8e feat(opensearch): add abilility to add libremdb as search engine
thanks to @user451421541757324

re https://github.com/zyachel/libremdb/issues/95
2025-06-10 09:47:49 +03:00
0e2117e4c7 fix(lockfile): update lockfile 2025-06-10 09:47:42 +03:00
ec6c0a9a13 chore(release): 4.2.0 2025-06-10 09:47:33 +03:00
34bcdc3b05 fix(name,title): handle empty states in "did you know" section 2025-06-10 09:47:11 +03:00
e98ab85034 feat(title): add "interests" in basic info 2025-06-10 09:47:05 +03:00
255fbb521f fix(title): fix title route crash
was due to an upstream change.
also show ai summary, ratings distrubution, and multiple user
reviews

re https://github.com/zyachel/libremdb/issues/98
2025-06-10 09:46:57 +03:00
ac60bda3bd fix(error): add trace to browser in dev mode
also make AppError a bit easier to use
2025-06-10 09:46:37 +03:00
ngn
bde980536d fix the example docker compose file
Some checks failed
ups / ups (push) Failing after 39s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:51:13 +03:00
ngn
e6ebf6ca78 remove the depend docker command
All checks were successful
docker / docker (push) Successful in 1m39s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:43:26 +03:00
ngn
9a16fa65c9 attempt to fix the broken pnpm docker command
All checks were successful
docker / docker (push) Successful in 1m46s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:35:05 +03:00
ngn
8ba89d1885 fix the pnpm-lock issue
Some checks failed
docker / docker (push) Failing after 12s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:29:21 +03:00
ngn
10772a8d6f add the ups workflow
Some checks failed
docker / docker (push) Failing after 20s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:09:41 +03:00
ngn
22bba72cf9 add renovate config
All checks were successful
Build and publish the docker image / build (push) Successful in 1m11s
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-20 00:41:58 +03:00
ngn
d9c37b7f8f add footer to the layout
All checks were successful
Build and publish the docker image / build (push) Successful in 1m20s
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-20 00:31:02 +03:00
ngn
19a74ec438 fix missing border-bottom semicolon for header
All checks were successful
Build and publish the docker image / build (push) Successful in 1m19s
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-20 00:25:03 +03:00
ngn
1132ba4a98 add PUBLIC_TITLE env and cleanup example env file
All checks were successful
Build and publish the docker image / build (push) Successful in 1m19s
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-19 23:48:30 +03:00
ngn
ff8ca7db4a [skip ci] fix example docker compose
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-19 23:37:48 +03:00
ngn
e1d46adb1e fix theme selection
All checks were successful
Build and publish the docker image / build (push) Successful in 1m22s
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-19 23:34:29 +03:00
ngn
721d83eff1 fix footer link loop and update theme colors
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-19 23:32:01 +03:00
ngn
2b13a79013 update footer links
Some checks failed
Build and publish the docker image / build (push) Failing after 40s
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-19 23:18:56 +03:00
ngn
d1e2d1fa1f general cleanup
Signed-off-by: ngn <ngn@ngn.tf>
2025-01-19 23:13:30 +03:00
70efba181f ci(release): add ability to trigger action manually as well 2024-12-15 09:38:10 +00:00
dfc45f4e4a chore(release): 4.1.0 2024-12-15 15:04:10 +05:30
630aae3cd8 docs(instances): add a new instance
by @JodanJodan

closes: https://github.com/zyachel/libremdb/issues/93
2024-12-15 09:32:56 +00:00
250d8bb388 Merge branch 'main' of github:zyachel/libremdb 2024-12-15 09:23:57 +00:00
1030798a73 fix(reviews): revert implementation of reviews route
have to rewrite it later

re: https://github.com/zyachel/libremdb/issues/91
2024-12-15 09:23:41 +00:00
f141f45f96 Merge pull request #89 from jollySleeper/workflow
Workflow for Docker Images
2024-12-14 12:11:57 +00:00
26769fd7c0 docs(installation): docker image installation steps 2024-12-08 19:15:33 +05:30
82e530e82b Merge pull request #92 from caninetools/main
add libremdb.canine.tools
2024-12-01 20:57:42 +00:00
4eb30dc534 add libremdb.canine.tools 2024-12-01 12:16:38 -05:00
8465103877 docs(installation): update docker image 2024-11-25 03:26:00 +05:30
4caf603162 feat: github workflow for building container images 2024-11-25 03:19:57 +05:30
47 changed files with 3179 additions and 3416 deletions

12
.env.example Normal file
View File

@ -0,0 +1,12 @@
NEXT_PUBLIC_URL=https://example.com
NEXT_PUBLIC_TITLE='my libremdb'
AXIOS_USERAGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3'
AXIOS_LANGUAGE='en-US,en;q=0.5'
AXIOS_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
# USE_REDIS=true
# USE_REDIS_FOR_API_ONLY=true
# REDIS_CACHE_TTL_API=3600
# REDIS_CACHE_TTL_MEDIA=3600
# REDIS_URL=localhost:6379

View File

@ -1,43 +0,0 @@
################################################################################
### PLEASE FILL/ENABLE REQUIRED VARS AT LEAST BEFORE RUNNING THE APPLICATION ###
################################################################################
################################################################################
### 1. REQUIRED VARS(site may not work as expected without these).
################################################################################
## used for meta tags. e.g: 'https://libremdb.iket.me'. don't add end slash.
NEXT_PUBLIC_URL=
## used when fetching data from IMDb. not adding these could result in not getting any response.
## example useragent header: 'Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0'
AXIOS_USERAGENT=
## example accept header: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
AXIOS_ACCEPT=
################################################################################
### 2. OPTIONAL VARS(enabling these is encouraged)
################################################################################
## for forcing a certain language for data we get from imdb. Useful when you don't want your IP to determine the preferred language.
# AXIOS_LANGUAGE='en-US,en;q=0.5'
## comment it out if you wish to enable nextjs stats collection. more at https://nextjs.org/telemetry
NEXT_TELEMETRY_DISABLED=1
################################################################################
### 3. REDIS CONFIG(optional if you don't need redis)
################################################################################
## enables caching of api routes as well as media
# USE_REDIS=true
## in case you don't want to cache media but only api routes
# USE_REDIS_FOR_API_ONLY=true
## ttl for media and api
# REDIS_CACHE_TTL_API=3600
# REDIS_CACHE_TTL_MEDIA=3600
## for docker, just set the domain to the container name, default is 'libremdb_redis'
# REDIS_URL=localhost:6379
################################################################################
### 4. INSTANCE META FIELDS(not required but good to have)
################################################################################
## example: 'https://iket.me'.
NEXT_PUBLIC_INSTANCE_MAIN_URL=
## eg: 'zyachel'
NEXT_PUBLIC_INSTANCE_NAME=

View File

@ -0,0 +1,34 @@
name: docker
on:
push:
branches:
- "main"
paths-ignore:
- "README.md"
- "LICENSE.txt"
- "docker-compose.example.yml"
- "ups.json"
env:
REGISTRY: git.ngn.tf
IMAGE: ${{gitea.repository}}
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to container repo
uses: docker/login-action@v1
with:
registry: ${{env.REGISTRY}}
username: ${{gitea.actor}}
password: ${{secrets.PACKAGES_TOKEN}}
- name: Build docker image
run: |
docker build . --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest

25
.gitea/workflows/ups.yml Normal file
View File

@ -0,0 +1,25 @@
name: ups
on:
schedule:
- cron: "@weekly"
jobs:
ups:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt update -y
sudo apt install -y python3 python3-build python3-requests make
- name: Install ups
run: |
git clone https://git.ngn.tf/ngn/ups && cd ups
make && make install
- name: Run ups
run: PATH=~/.local/bin:$PATH ups-check

6
.gitignore vendored
View File

@ -24,7 +24,7 @@ yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
.env
# typescript
*.tsbuildinfo
@ -33,10 +33,10 @@ next-env.d.ts
#just dev stuff
dev/*
# other lockfiles
# other lockfiles
yarn.lock
package-lock.json
# docker
docker-compose.yml
dump.rdb
dump.rdb

View File

@ -1,154 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [4.0.0](https://github.com/zyachel/libremdb/compare/v3.4.0...v4.0.0) (2024-08-24)
### ⚠ BREAKING CHANGES
* **list:** will give 503 now
### Bug Fixes
* add disclaimer ([2626dfd](https://github.com/zyachel/libremdb/commit/2626dfd8ab75b6d05d20e92e8dc98be53017fe66))
* **api:** fix reviews api ([1041de5](https://github.com/zyachel/libremdb/commit/1041de5439604baab5fbc113c31dbad3096a2945))
* **cache:** make cache keys for review more distinctive ([324c138](https://github.com/zyachel/libremdb/commit/324c138ec49cd24e932f9b1f8569c22ca25ebc13))
* **cleaners:** fix app crash ([333d3b1](https://github.com/zyachel/libremdb/commit/333d3b107e36a8455364e5e09fc6064a9745f3b9))
* **error:** add sanity checks before error destructuring ([e320557](https://github.com/zyachel/libremdb/commit/e320557addd4f12f32a638f452a738b5a8f5aa32))
* **list:** remove list route ([67891c7](https://github.com/zyachel/libremdb/commit/67891c765533791a1a276e0669358b935ef9f697))
## [3.4.0](https://github.com/zyachel/libremdb/compare/v3.3.1...v3.4.0) (2024-03-31)
### Features
* **reviews:** add reviews route ([dc42b32](https://github.com/zyachel/libremdb/commit/dc42b3204caf843d0f07fa28572c5ed275bb601d))
### Bug Fixes
* **instances:** fix urls in instances.json ([3eb5178](https://github.com/zyachel/libremdb/commit/3eb517849f279b2453579d0b5c5000e803a13bca))
## [3.3.1](https://github.com/zyachel/libremdb/compare/v3.3.0...v3.3.1) (2024-01-06)
### Bug Fixes
* **dockerfile:** fix failing docker build ([719f42b](https://github.com/zyachel/libremdb/commit/719f42b5e6f6bafc0807986b6198dbbe1cb271ab))
## [3.3.0](https://github.com/zyachel/libremdb/compare/v3.2.0...v3.3.0) (2023-12-07)
### Features
* **api:** add a catch-all route ([9fdd731](https://github.com/zyachel/libremdb/commit/9fdd7311368411d59784977f77d1af103ae16543))
* **api:** add api endpoints for dynamic routes ([19f1700](https://github.com/zyachel/libremdb/commit/19f1700a55867c1fb8d6c11431bd4557e7520de1))
### Bug Fixes
* **api:** refactor all endpoints a bit ([4dffbbc](https://github.com/zyachel/libremdb/commit/4dffbbc0ec870a8f9a56e4ee62e6a6c472552f6a))
* **title:** fix title route crash ([a5b7d52](https://github.com/zyachel/libremdb/commit/a5b7d527833a67f40f992c13bbe391884c0d1f82))
## [3.2.0](https://github.com/zyachel/libremdb/compare/v3.1.1...v3.2.0) (2023-10-28)
### Features
* **list:** add list route ([97f1432](https://github.com/zyachel/libremdb/commit/97f1432ac5d23206229d806b7cb3e04af6dec36f))
## [3.1.1](https://github.com/zyachel/libremdb/compare/v3.1.0...v3.1.1) (2023-10-14)
### Bug Fixes
* **card:** fix long attributes in cards under 'Known For' section ([736d680](https://github.com/zyachel/libremdb/commit/736d6802430a3f4f364915f3df93fc548a51ebf1))
* **error:** fix incorrect 'view on IMDb' link on error page ([0aea2f4](https://github.com/zyachel/libremdb/commit/0aea2f47dad6eb78e319ea1abd8c444f2cba4424))
* **media proxy:** fix 304 response code with body error ([c610ef4](https://github.com/zyachel/libremdb/commit/c610ef4d1be39c122715a0eb200155537e7d6abf))
* **name:** fix name route crash ([38ed0c6](https://github.com/zyachel/libremdb/commit/38ed0c62177532b93f61af4172ffa6e5b9995bdc))
* **name:** fix route crash for some ids ([e91c313](https://github.com/zyachel/libremdb/commit/e91c313f127632f1bd44d190af71bc841bbe87b7))
* **title:** fix a crash in title route ([21a1c83](https://github.com/zyachel/libremdb/commit/21a1c83d95b703fa08cdb96c206626f22d5366c9))
## [3.1.0](https://github.com/zyachel/libremdb/compare/v3.0.0...v3.1.0) (2023-05-21)
### Features
* **cache:** implement caching of routes ([c53c88d](https://github.com/zyachel/libremdb/commit/c53c88db9bf98258547e2ca512f864800821cb1f))
### Bug Fixes
* **form:** fix hydration error ([8599ae2](https://github.com/zyachel/libremdb/commit/8599ae2c5ac11f2818f56c9f7de7666a38b4386c))
* **name:** fix a couple of crashes in name and title route ([8d9b663](https://github.com/zyachel/libremdb/commit/8d9b6630a576b7e8331eb5431cd90d02733b4917))
## [3.0.0](https://github.com/zyachel/libremdb/compare/v2.4.0...v3.0.0) (2023-04-15)
### ⚠ BREAKING CHANGES
* **title:** older versions won't work, at least for title route
### Features
* add info related to the current instance ([2c5d2f8](https://github.com/zyachel/libremdb/commit/2c5d2f86e46a52223f07d573b152bad5174ee2d9))
* **route:** add name route ([75732e0](https://github.com/zyachel/libremdb/commit/75732e00869f9777e87e767a48648996345f02f7))
### Bug Fixes
* **title:** fix title page crash ([8ce02d0](https://github.com/zyachel/libremdb/commit/8ce02d02364c8e1f03a8b16594bc20ee6766a8c6))
# [2.4.0](https://github.com/zyachel/libremdb/compare/v2.3.1...v2.4.0) (2023-01-22)
### Bug Fixes
* fix app crash ([71d1d5b](https://github.com/zyachel/libremdb/commit/71d1d5b34e2866729ae0c96c59ea51e8d1a3dcca))
### Features
* add error boundary ([5cc2ef0](https://github.com/zyachel/libremdb/commit/5cc2ef02cec0b31c5d449e189a054fbef5801f60))
## [2.3.1](https://github.com/zyachel/libremdb/compare/v2.3.0...v2.3.1) (2023-01-15)
### Bug Fixes
* fix unseekable videos on webkit-based browsers ([a32785c](https://github.com/zyachel/libremdb/commit/a32785ce00b638e9079f0924fd9b00f98c077348))
# [2.3.0](https://github.com/zyachel/libremdb/compare/v2.2.2...v2.3.0) (2022-12-31)
### Bug Fixes
* couple css improvements for webkit-based browsers ([81eaf2f](https://github.com/zyachel/libremdb/commit/81eaf2fd5e5980c0c4d59a8805cf541fa8fe51f9))
### Features
* **search:** add basic search functionality ([0cff34a](https://github.com/zyachel/libremdb/commit/0cff34a766b09ba17be2a89f6290889dbf225436))
## [2.2.2](https://github.com/zyachel/libremdb/compare/v2.2.1...v2.2.2) (2022-12-10)
### Bug Fixes
* app crash on qutebrowser ([78b14ec](https://github.com/zyachel/libremdb/commit/78b14ec07955d29403b8b5ae0d449f38eea2bbc5))
## [2.2.1](https://github.com/zyachel/libremdb/compare/v2.2.0...v2.2.1) (2022-12-01)
### Bug Fixes
* **title:** fix site crash ([dd75df0](https://github.com/zyachel/libremdb/commit/dd75df01eb7c03d8945a8bd20ed231a66bd88b8f))

View File

@ -1,35 +1,25 @@
# Thanks @yordis on Github! https://github.com/vercel/next.js/discussions/16995#discussioncomment-132339
# Install dependencies only when needed
FROM node:lts-alpine AS deps
WORKDIR /opt/app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
# Rebuild the source code only when needed
# This is where because may be the case that you would try
# to build the app based on some `X_TAG` in my case (Git commit hash)
# but the code hasn't changed.
FROM node:lts-alpine AS builder
ENV NODE_ENV=production
WORKDIR /opt/app
RUN npm install -g pnpm
WORKDIR /app
COPY . .
COPY --from=deps /opt/app/node_modules ./node_modules
ENV NODE_ENV=production
RUN npm install -g pnpm
RUN pnpm install
RUN pnpm build
# Production image, copy all the files and run next
FROM gcr.io/distroless/nodejs18-debian11 AS runner
ARG X_TAG
WORKDIR /opt/app
FROM node:lts-alpine AS runner
WORKDIR /app
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
COPY --from=builder /opt/app/next.config.mjs ./
COPY --from=builder /opt/app/public ./public
COPY --from=builder /opt/app/.next ./.next
COPY --from=builder /opt/app/node_modules ./node_modules
ENV HOST=0.0.0.0
ENV PORT=3000
CMD ["./node_modules/next/dist/bin/next", "start"]
CMD ["./node_modules/next/dist/bin/next", "start"]

223
README.md
View File

@ -1,220 +1,7 @@
# libremdb
# libremdb - IMDb frontend
A free & open source IMDb front-end.
![](https://git.ngn.tf/ngn/libremdb/actions/workflows/docker.yml/badge.svg)
![](https://git.ngn.tf/ngn/libremdb/actions/workflows/ups.yml/badge.svg)
Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter](https://github.com/zedeus/nitter) and [many others](#similar-projects).
| | |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| <img src="./public/img/misc/preview.jpg" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.jpg" title="screenshot (mobile screen, dark mode)" width="400" /> |
---
## Some Features
- No ads or tracking
Browse any movie info without being tracked or bombarded by annoying ads.
- Modern interface
Modern interface with curated colors supporting both dark and light themes.
- Responsive design
Be it your small mobile or big computer screen, it's fully responsive.
- Lightweight
_[Up movie page](https://imdb.com/title/tt1049413/)_
(tested on Firefox v104; without scroll; simulated regular 4g)
| Network tab stats | libremdb | IMDb |
| ------------------------ | -------- | ------ |
| no. of requests | 22 | 180 |
| data transfered(gzipped) | 468KB | 1.88MB |
| load event fired in | 6.22s | 10.01s |
---
## Instances
| Instance | Tor | I2P | Region | Cloudflare | Notes |
| ---------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------- | ---------- | ------------------------------------------------------------------ |
| <https://libremdb.iket.me/> | No | No | CA | No | Operated by me |
| <https://libremdb.pussthecat.org/> | No | No | DE | No | Operated by [PussTheCat.org](https://pussthecat.org/) |
| <https://ld.vern.cc/> | [Yes](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | [Yes](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p/) | US | No | Operated by [~vern](https://vern.cc/) |
| <https://binge.whatever.social/> | No | No | US/DE | No | Operated by [Whatever Social](https://whatever.social/) |
| <https://libremdb.lunar.icu/> | No | No | DE | No | Operated by [lunar.icu](https://lunar.icu/) |
| <https://libremdb.jeikobu.net/> | No | No | DE | Yes | Operated by [shindouj](https://github.com/shindouj/) |
| <https://lmdb.hostux.net/> | No | No | FR | No | Operated by [Hostux.net](https://hostux.net/) |
| <https://binge.whateveritworks.org/> | No | No | DE | Yes | Operated by [WhateverItWorks](https://github.com/WhateverItWorks/) |
| <https://libremdb.nerdyfam.tech/> | No | No | US | Yes | Operated by [Nerdyfam.tech](https://nerdyfam.tech/) |
| <https://libremdb.tux.pizza/> | No | No | US | No | Operated by [tux.pizza](https://tux.pizza/) |
| <https://libremdb.frontendfriendly.xyz/> | No | No | &mdash; | No | Operated by [frontendfriendly.xyz](https://frontendfriendly.xyz/) |
| <https://d.opnxng.com/> | No | No | SG | No | Operated by [Opnxng](https://about.opnxng.com/) |
| <https://libremdb.catsarch.com/> | [Yes](https://libremdb.catsarchywsyuss6jdxlypsw5dc7owd5u5tr6bujxb7o6xw2hipqehyd.onion) | [Yes](http://qjlgasoy3nxepgzntucmcnb6pryqxakwdu7sxvqzi7spdfootryq.b32.i2p/) | US | No | Operated by [Butter Cat](https://catsarch.com/) |
| <https://libremdb.r4fo.com/> | [Yes](http://libremdb.r4focoma7gu2zdwwcjjad47ysxt634lg73sxmdbkdozanwqslho5ohyd.onion/) | No | NL | No | Operated by [r4fo](https://r4fo.com/) |
| <https://libremdb.privacydev.net/> | [Yes](http://libremdb.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | No | FR | No | Operated by [PrivacyDev](https://privacydev.net/) |
| <https://libremdb.ducks.party/> | No | No | NL | No | Operated by [ducks.party](https://ducks.party/) |
| <https://lmdb.ngn.tf/> | No | No | TR | No | Operated by [ngn](https://ngn.tf/) |
| <https://lmdb.bloat.cat/> | No | No | DE | No | Operated by [bloat.cat](https://bloat.cat/) |
| <https://libremdb.darkness.services/> | No | No | US | Yes | Operated by [blade10101](https://github.com/blade10101) |
| <https://libremdb.hyperreal.coffee/> | No | No | US | No | Operated by [hyperreal64](https://github.com/hyperreal64) |
| <https://ld.ca.zorby.top/> | [Yes](http://q3hetdcyyy572xznqmsledzlbv77moycoqs6ptehpp5vsmx4dtcuqeqd.onion/) | [Yes](http://5j37qusybvyhecljn4hr5i4chifdlfqfkfveythzpzyfxiibt7cq.b32.i2p/) | CA | No | Operated by [Troughy](https://zorby.top/) |
| <https://imdb.nerdvpn.de/> | No | No | UA | No | Operated by [Weidenwiesel](https://nerdvpn.de/) |
Instances list in JSON format can be found in [instances.json](instances.json) file.
---
## Questions you might have
- How do I use it?
Replace `imdb.com` in any IMDb URL with any of the instances. For example: '[imdb.com/title/tt1049413](https://imdb.com/title/tt1049413/)' to '[libremdb.iket.me/title/tt1049413](https://libremdb.iket.me/title/tt1049413/)'.
To avoid changing the URLs manually, you can use [extensions](#automatic-redirection).
- Why is it so slow?
Whenever you request info about a movie/show on libremdb, 4 trips are made(2 between your browser and libremdb's server, and 2 between libremdb's server and IMDb's server) instead of the usual 2 trips when you visit a website. For this reason there's a noticable delay. This is a bit of inconvenience you'll have to face should you wish to use this website.
- It doesn't have all routes.
I'll implement more with time :)
- Is content served from third-parties, like Amazon?
Nope, libremdb proxies all image and video requests through the instance to avoid exposing your IP address, browser information and other personally identifiable metadata ([Contributor](https://github.com/httpjamesm)).
- Why not just use IMDb?
Refer to the [features section](#some-features) above.
- Why didn't you use other databases like [TMDB](https://www.themoviedb.org/) or [OMDb](https://www.omdbapi.com/)?
IMDb simply has superior dataset compared to all other alternatives. With that being said, I'd encourage you to check out those alternatives too.
---
## Privacy
- Information collected:
None.
- Information stored in your browser:
A key named 'theme' is stored in Local Storage provided by your browser, if you ever override the default theme. To remove it, go to site data settings, and clear the data for this website. To permamently disable libremdb from storing your theme prefrences, either turn off JavaScript or disable access to Local Storage for libremdb.
- Information collected by other services:
None. libremdb proxies images anonymously through the instance for maximum privacy ([Contributor](https://github.com/httpjamesm)).
---
## To-Do
- [ ] add advanced search route
- [x] add did you know and reviews on movie info page
- [x] add a way to see trailer and other videos
- [ ] implement movie specific routes like:
- [x] reviews(including critic reviews)
- [ ] video & image gallery
- [ ] sections under 'did you know'
- [ ] release info
- [ ] parental guide
- [ ] implement other routes like:
- [x] lists
- [ ] moviemeter
- [x] person info(includes directors and actors)
- [ ] company info
- [ ] user info
- [x] use redis, or any other caching strategy
- [x] implement a better installation method
- [x] serve images and videos from libremdb itself
---
## Installation
As libremdb is made with Next.js, you can deploy it anywhere where Next.js is supported. Below are a few other methods:
### Manual
1. Install Node.js and Git.
for Node.js, visit [their website](https://nodejs.org/en/).
for Git, run `sudo apt install git` if you're on a Debian-based distro. Else visit [their website](https://git-scm.com/).
2. Install redis(optional).
You can install redis from [here](https://redis.io).
3. Clone and set up the repo.
```bash
git clone https://github.com/zyachel/libremdb.git # replace github.com with codeberg.org if you wish so.
cd libremdb
# change the configuration file to your liking.
cp .env.local.example .env.local
# replace 'pnpm' with yarn or npm if you use those.
pnpm install
pnpm build
pnpm start
# optional: if you're using redis
redis-server
```
libremdb will start running at http://localhost:3000.
To change port, modify the last command like this: `pnpm start -- -p <port-number>`.
### Docker (Local)
You can build the docker image using the provided Dockerfile(thanks to [@httpjamesm](https://github.com/httpjamesm)) and set it up using the [example docker-compose file](./docker-compose.example.yml).
Change the docker-compose file to your liking and run `docker-compose up -d` to start the container, that's all!
### Docker (Built)
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that as well.
## Miscellaneous
### Automatic redirection
- [Redirector](https://github.com/einaregilsson/Redirector)
config:
```
Description: redirect IMDb to libremdb
Example URL: https://www.imdb.com/title/tt0258463/?ref_=tt_sims_tt_t_4
Include pattern: https?:\/\/(www\.)?imdb\.com\/(.*)
Redirect to: https://libremdb.iket.me/$2
Pattern type: Regular Expression
```
- [LibRedirect](https://github.com/libredirect/libredirect/)
- [Privacy Redirector](https://github.com/dybdeskarphet/privacy-redirector)
### Similar projects
- [Teddit](https://codeberg.org/teddit/teddit)
Teddit is an alternative Reddit front-end focused on privacy.
- [Nitter](https://github.com/zedeus/nitter)
Nitter is a free and open source alternative Twitter front-end focused on privacy.
- [Bibliogram](https://sr.ht/~cadence/bibliogram/)
Bibliogram is an alternative front-end for Instagram.
- [Invidious](https://invidious.io)
Invidious is an alternative front-end to YouTube.
- [Libreddit](https://github.com/spikecodes/libreddit)
Libreddit is an alternative private front-end to Reddit.
- [Scribe](https://git.sr.ht/~edwardloveall/scribe)
Scribe is an alternative Medium frontend.
- [full list &rarr;](https://github.com/digitalblossom/alternative-frontends)
---
## Contact
I'm availabe on [[matrix]](https://matrix.to/#/@ninal:matrix.org) and [email](mailto:aricla@protonmail.com) in case you wish to contact me personally.
---
## License
Licensed under GNU AGPLv3.
See [License](./LICENSE) for full legalese.
---
## Disclaimer
_libremdb does not host any content. All content on libremdb is from IMDb. IMDb is a trademark of IMDb.com, Inc._
A fork of the [libremdb](https://github.com/zyachel/libremdb) project, with my
personal changes.

View File

@ -1,47 +1,31 @@
# docker-compose.yml
version: '3'
services:
libremdb:
container_name: libremdb
build:
context: .
dockerfile: Dockerfile
image: git.ngn.tf/ngn/libremdb
ports:
- "3000:3000"
env_file: .env.local.example
- 80:3000
env_file: .env.example
depends_on:
- libremdb-redis
restart: always
- libremdb_redis
tmpfs:
- /app/.next/cache/:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: 65534:65534 # equivalent to the nobody user
read_only: true
tmpfs:
- /opt/app/.next/cache/:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
networks:
- libremdb
restart: unless-stopped
libremdb-redis:
libremdb_redis:
container_name: libremdb_redis
image: redis
# FOR DEBUGGING ONLY
# ports:
# - "6379:6379"
restart: always
user: nobody
read_only: true
security_opt:
- no-new-privileges:true
tmpfs:
- /data:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
networks:
- libremdb
networks:
libremdb:
read_only: true
restart: unless-stopped

View File

@ -115,5 +115,15 @@
"clearnet": "https://imdb.nerdvpn.de/",
"cdn": false,
"country": "UA"
},
{
"clearnet": "https://libremdb.canine.tools/",
"cdn": false,
"country": "US"
},
{
"clearnet": "https://libremdb-fly.fly.dev/",
"cdn": false,
"country": "US"
}
]

View File

@ -9,6 +9,11 @@ const nextConfig = {
destination: '/find',
permanent: true,
},
{
source: '/:langcode(\\w{2})/:slug*',
destination: '/:slug*',
permanent: true,
},
];
},
images: {

View File

@ -1,6 +1,6 @@
{
"name": "libremdb",
"version": "4.0.0",
"version": "4.2.0",
"description": "a free & open source IMDb front-end",
"private": true,
"type": "module",
@ -18,7 +18,7 @@
},
"dependencies": {
"axios": "^0.27.2",
"cheerio": "1.0.0-rc.12",
"cheerio": "1.1.0",
"ioredis": "^5.3.2",
"next": "12.2.5",
"react": "18.2.0",

3200
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- sharp

7
public/opensearch.xml Normal file
View File

@ -0,0 +1,7 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>libremdb</ShortName>
<Description>Search libremdb</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">https://libremdb.iket.me/favicon.ico</Image>
<Url type="text/html" method="get" template="https://libremdb.iket.me/find?q={searchTerms}"/>
</OpenSearchDescription>

4
renovate.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": ["config:recommended"],
"prHourlyLimit": 20
}

View File

@ -1,34 +0,0 @@
import { useContext } from 'react';
import { themeContext } from 'src/context/theme-context';
import styles from 'src/styles/modules/components/buttons/themeToggler.module.scss';
type Props = {
className: string;
};
const ThemeToggler = (props: Props) => {
const { theme, setTheme } = useContext(themeContext);
const clickHandler = () => {
const themeToSet = theme === 'light' ? 'dark' : 'light';
setTheme(themeToSet);
};
return (
<button
className={`${styles.button} ${props.className}`}
onClick={clickHandler}
>
<span className='visually-hidden'>Change theme</span>
<svg
className={`icon ${styles.icon}`}
focusable='false'
aria-hidden='true'
role='img'
>
<use href='/svg/sprite.svg#icon-theme-switcher'></use>
</svg>
</button>
);
};
export default ThemeToggler;

View File

@ -11,6 +11,7 @@ type Props = {
message: string;
statusCode?: number;
originalPath?: string;
stack?: string;
/** props specific to error boundary. */
misc?: {
subtext: string;
@ -19,7 +20,9 @@ type Props = {
};
};
const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
const isDev = process.env.NODE_ENV === 'development';
const ErrorInfo = ({ message, statusCode, misc, originalPath, stack }: Props) => {
const title = statusCode ? `${message} (${statusCode})` : message;
return (
<>
@ -39,6 +42,11 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
<use href='/svg/sadgnu.svg#sad-gnu'></use>
</svg>
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
{Boolean(stack && isDev) && (
<pre className={styles.stack}>
<code>{stack}</code>
</pre>
)}
{misc ? (
<>
<p>{misc.subtext}</p>
@ -64,6 +72,13 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
.
</p>
)}
<p>
If you think this shouldn't happen,{' '}
<Link href='/contact'>
<a className='link'>let it be known</a>
</Link>
.
</p>
</Layout>
</>
);

View File

@ -1,27 +1,22 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import styles from 'src/styles/modules/layout/footer.module.scss';
const links = [
{ path: '/about', text: 'About' },
{ path: '/find', text: 'Find' },
{ path: '/privacy', text: 'Privacy' },
{ path: '/contact', text: 'Contact' },
{ url: 'https://github.com/zyachel/libremdb', text: 'Source' },
{ url: 'https://git.ngn.tf/ngn/libremdb', text: 'Modified Source' },
] as const;
const Footer = () => {
const { pathname } = useRouter();
return (
<footer id='footer' className={styles.footer}>
<nav aria-label='primary navigation' className={styles.nav}>
<ul className={styles.list}>
{links.map(link => (
<li className={styles.nav__item} key={link.path}>
<Link href={link.path}>
<li className={styles.nav__item} key={link.url}>
<Link href={link.url}>
<a
className={styles.nav__link}
aria-current={pathname === link.path ? 'page' : undefined}
aria-current={undefined}
>
{link.text}
</a>
@ -35,10 +30,6 @@ const Footer = () => {
</li>
</ul>
</nav>
<em className={styles.licence}>
libremdb does not host any content. All content on libremdb is from IMDb. IMDb is a
trademark of IMDb.com, Inc.
</em>
</footer>
);
};

View File

@ -1,5 +1,4 @@
import Link from 'next/link';
import ThemeToggler from 'src/components/buttons/ThemeToggler';
import styles from 'src/styles/modules/layout/header.module.scss';
type Props = { full?: boolean; originalPath?: string };
@ -52,7 +51,6 @@ const Header = ({ full, originalPath }: Props) => {
</svg>
</a>
</Link>
<ThemeToggler className={styles.themeToggler} />
</div>
</div>
{full && (

View File

@ -14,32 +14,41 @@ type Props = {
const Media = ({ className, media }: Props) => {
return (
<div className={`${className} ${styles.media}`}>
{(media.trailer || !!media.videos.total) && (
{(media.trailers?.length || !!media.videos.total) && (
<section className={styles.videos}>
<h2 className='heading heading__secondary'>Videos</h2>
<div className={styles.videos__container}>
{media.trailer && (
<div className={styles.trailer}>
{media.trailers?.map(trailer => (
<div className={styles.trailer} key={trailer.id}>
<video
aria-label='trailer video'
aria-label={trailer.caption ?? 'trailer video'}
controls
playsInline
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
poster={getProxiedIMDbImgUrl(modifyIMDbImg(trailer.thumbnail))}
className={styles.trailer__video}
preload='none'
muted
>
{media.trailer.urls.map(source => (
{trailer.urls.map(source => (
<source
key={source.url}
type={source.mimeType ?? undefined}
type='video/mp4'
src={getProxiedIMDbImgUrl(source.url)}
media={source.resolution !== 'SD' ? '(min-width: 450px)' : undefined}
data-res={source.resolution}
/>
))}
<p>
{trailer.caption}:{' '}
<Link href={getProxiedIMDbImgUrl(trailer.urls[0]?.url)}>
<a className='link'>link</a>
</Link>
</p>
</video>
</div>
)}
))}
{!!media.videos.total &&
media.videos.videos.map(video => (

View File

@ -1,5 +1,4 @@
import Head from 'next/head';
import { ReactNode } from 'react';
type Props = {
title: string;
@ -8,6 +7,7 @@ type Props = {
};
const BASE_URL = process.env.NEXT_PUBLIC_URL ?? 'https://iket.me';
const BASE_TITLE = process.env.NEXT_PUBLIC_TITLE ?? 'libremdb';
const Meta = ({
title,
@ -22,7 +22,7 @@ const Meta = ({
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title key='title'>{`${title} | libremdb`}</title>
<title key='title'>{`${title} - ${BASE_TITLE}`}</title>
<meta key='desc' name='description' content={description} />
<link rel='icon' href='/favicon.ico' sizes='any' />
<link rel='icon' href='/icon.svg' type='image/svg+xml' />
@ -36,6 +36,13 @@ const Meta = ({
<meta property='og:locale' content='en_US' />
<meta property='og:type' content='video.movie' />
<meta property='og:image' content={url.toString()} />
<link
rel='search'
type='application/opensearchdescription+xml'
href='/opensearch.xml'
title='libremdb'
></link>
</Head>
);
};

View File

@ -9,43 +9,54 @@ type Props = {
const DidYouKnow = ({ data }: Props) => (
<section className={styles.container}>
<h2 className='heading heading__secondary'>Did you know</h2>
{!!data.trivia?.total && (
<section>
<h3 className='heading heading__tertiary'>Trivia</h3>
<div dangerouslySetInnerHTML={{ __html: data.trivia.html }}></div>
</section>
)}
{!!data.quotes?.total && (
<section>
<h3 className='heading heading__tertiary'>Quotes</h3>
<div dangerouslySetInnerHTML={{ __html: data.quotes.html }}></div>
</section>
)}
{!!data.trademark?.total && (
<section>
<h3 className='heading heading__tertiary'>Trademark</h3>
<div dangerouslySetInnerHTML={{ __html: data.trademark.html }}></div>
</section>
)}
{!!data.nicknames.length && (
<section>
<h3 className='heading heading__tertiary'>Nicknames</h3>
<p>{data.nicknames.join(', ')}</p>
</section>
)}
{!!data.salary?.total && (
<section>
<h3 className='heading heading__tertiary'>Salary</h3>
<p>
<span>{data.salary.value} in </span>
<Link href={`/title/${data.salary.title.id}`}>
<a className={'link'}>{data.salary.title.text}</a>
</Link>
<span> ({data.salary.title.year})</span>
</p>
</section>
{isEmpty(data) ? (
<p>Nothing interesting to show.</p>
) : (
<>
{!!data.trivia?.total && (
<section>
<h3 className='heading heading__tertiary'>Trivia</h3>
<div dangerouslySetInnerHTML={{ __html: data.trivia.html }}></div>
</section>
)}
{!!data.quotes?.total && (
<section>
<h3 className='heading heading__tertiary'>Quotes</h3>
<div dangerouslySetInnerHTML={{ __html: data.quotes.html }}></div>
</section>
)}
{!!data.trademark?.total && (
<section>
<h3 className='heading heading__tertiary'>Trademark</h3>
<div dangerouslySetInnerHTML={{ __html: data.trademark.html }}></div>
</section>
)}
{!!data.nicknames.length && (
<section>
<h3 className='heading heading__tertiary'>Nicknames</h3>
<p>{data.nicknames.join(', ')}</p>
</section>
)}
{!!data.salary?.total && (
<section>
<h3 className='heading heading__tertiary'>Salary</h3>
<p>
<span>{data.salary.value} in </span>
<Link href={`/title/${data.salary.title.id}`}>
<a className={'link'}>{data.salary.title.text}</a>
</Link>
<span> ({data.salary.title.year})</span>
</p>
</section>
)}
</>
)}
</section>
);
export default DidYouKnow;
const isEmpty = (data: Props['data']) =>
Boolean(
!data.nicknames.length && !data.quotes && !data.salary && !data.trademark && !data.trivia
);

View File

@ -86,6 +86,21 @@ const Basic = ({ data, className }: Props) => {
))}
</p>
)}
{!!data.interests.length && (
<p className={styles.genres}>
<span className={styles.genres__heading}>Interests: </span>
{data.interests.map((interest, i) => (
<Fragment key={interest.id}>
{i > 0 && ', '}
<Link href={`/interest/${interest.id}`}>
<a className={styles.link}>{interest.text}</a>
</Link>
</Fragment>
))}
</p>
)}
<p className={styles.overview}>
<span className={styles.overview__heading}>Plot: </span>
<span className={styles.overview__text}>{data.plot || '-'}</span>

View File

@ -7,7 +7,13 @@ type Props = {
};
const DidYouKnow = ({ data }: Props) => {
if (!Object.keys(data).length) return <></>;
if (!Object.keys(data).length)
return (
<section className={styles.didYouKnow}>
<h2 className='heading heading__secondary'>Did you know</h2>
<p>Nothing interesting to show.</p>
</section>
);
return (
<section className={styles.didYouKnow}>
<h2 className='heading heading__secondary'>Did you know</h2>

View File

@ -1,82 +1,127 @@
import { useRouter } from 'next/router';
import Link from 'next/link';
import { Reviews } from 'src/interfaces/shared/title';
import type { Reviews as TReviews } from 'src/interfaces/shared/title';
import { formatNumber } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/title/reviews.module.scss';
type Props = {
reviews: Reviews;
reviews: TReviews;
};
const Reviews = ({ reviews }: Props) => {
return (
<section className={styles.reviews}>
<h2 className='heading heading__secondary'>Reviews</h2>
<RatingsDistribution ratings={reviews.ratingsDistribution} />
<section className={styles.userReviews}>
<h3 className='heading heading__tertiary'>User Reviews</h3>
{reviews.featuredReviews ? (
<ul className={styles.userReviews__list} role='list'>
{reviews.featuredReviews.map(featuredReview => (
<li key={featuredReview.id}>
<details className={styles.review}>
<summary className={styles.review__summary}>
<strong>{featuredReview.review.summary}</strong>
</summary>
<div
className={styles.review__text}
dangerouslySetInnerHTML={{
__html: featuredReview.review.html,
}}
></div>
<footer className={styles.review__metadata}>
<p>
{featuredReview.rating && <span>Rated {featuredReview.rating}/10</span>}
<span>
{' '}
by{' '}
<Link href={`/user/${featuredReview.reviewer.id}`}>
<a className='link'>{featuredReview.reviewer.name}</a>
</Link>
</span>
</p>
</footer>
</details>
</li>
))}
</ul>
) : (
<p>No reviews yet.</p>
)}
</section>
{reviews.ai?.summary && (
<details className={styles.reviewAi}>
<summary className='heading heading__tertiary'>AI Summary</summary>
<p dangerouslySetInnerHTML={{ __html: reviews.ai.summary }} />
<ul>
{reviews.ai.themes.map(theme => (
<li key={theme.id}>{theme.text}</li>
))}
</ul>
</details>
)}
<ReviewStats reviews={reviews} />
</section>
);
};
export default Reviews;
const RatingsDistribution = ({ ratings }: { ratings: Props['reviews']['ratingsDistribution'] }) => {
const maxRating = Math.max(...ratings.map(r => r.votes));
return (
<div className={styles.ratingsDistribution}>
<h3 className='heading heading__tertiary'>Ratings Distribution</h3>
{ratings.length ? (
<ul>
{ratings.map(rating => (
<li
key={rating.rating}
style={
{
'--bar-height': `${((rating.votes / maxRating) * 100).toFixed(2)}%`,
} as React.CSSProperties
}
>
<span>
{rating.rating} <span>({formatNumber(rating.votes)})</span>
</span>
</li>
))}
</ul>
) : (
<p>No ratings yet.</p>
)}
</div>
);
};
const ReviewStats = ({ reviews }: { reviews: Props['reviews'] }) => {
const router = useRouter();
const { titleId } = router.query;
return (
<section className={styles.reviews}>
<h2 className="heading heading__secondary">Reviews</h2>
{reviews.featuredReview && (
<article className={styles.reviews__reviewContainer}>
<details className={styles.review}>
<summary className={styles.review__summary}>
<strong>{reviews.featuredReview.review.summary}</strong>
</summary>
<div
className={styles.review__text}
dangerouslySetInnerHTML={{
__html: reviews.featuredReview.review.html,
}}
></div>
</details>
<footer className={styles.review__metadata}>
<p>
{reviews.featuredReview.rating && (
<span>Rated {reviews.featuredReview.rating}/10</span>
)}
<span>
{' '}
by{' '}
<Link href={`/user/${reviews.featuredReview.reviewer.id}`}>
<a className="link">{reviews.featuredReview.reviewer.name}</a>
</Link>
</span>
<span> on {reviews.featuredReview.date}.</span>
</p>
<p>
<span>
{formatNumber(reviews.featuredReview.votes.up)} upvotes
</span>
<span>
, {formatNumber(reviews.featuredReview.votes.down)} downvotes
</span>
</p>
</footer>
</article>
)}
<div className={styles.reviews__stats}>
<p>
<Link href={`/title/${titleId}/reviews`}>
<a className="link">
{formatNumber(reviews.numUserReviews)} User reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/externalreviews`}>
<a className="link">
{formatNumber(reviews.numCriticReviews)} Critic reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/criticreviews`}>
<a className="link"> {reviews.metacriticScore} Metascore</a>
</Link>
</p>
</div>
</section>
<div className={styles.reviewStats}>
<p>
<Link href={`/title/${titleId}/reviews`}>
<a className='link'>{formatNumber(reviews.numUserReviews)} User reviews</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/externalreviews`}>
<a className='link'>{formatNumber(reviews.numCriticReviews)} Critic reviews</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/criticreviews`}>
<a className='link'> {reviews.metacriticScore} Metascore</a>
</Link>
</p>
</div>
);
};
export default Reviews;

View File

@ -143,7 +143,7 @@ export default interface Name {
value: string;
language: string;
};
mimeType?: string;
videoMimeType?: string;
url: string;
}>;
recommendedTimedTextTrack?: {

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
import { AppError as AppErrorClass } from 'src/utils/helpers';
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
export type AppError = Pick<InstanceType<typeof AppErrorClass>, 'message' | 'statusCode' | 'stack'>;

View File

@ -1,205 +0,0 @@
import Link from 'next/link';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import styles from 'src/styles/modules/pages/about/about.module.scss';
const About = () => {
return (
<>
<Meta
title='About'
description='libremdb is a free & open source IMDb front-end. It allows you to see information about movies, tv shows, video games without any ads or tracking.'
/>
<Layout full className={styles.about}>
<section id='features' className={styles.features}>
<h2
className={`heading heading__secondary ${styles.features__heading}`}
>
Some features
</h2>
<ul className={styles.features__list}>
<li className={styles.feature}>
<svg
aria-hidden='true'
focusable='false'
role='img'
className={styles.feature__icon}
>
<use href='/svg/sprite.svg#icon-eye-slash'></use>
</svg>
<h3
className={`heading heading__tertiary ${styles.feature__heading}`}
>
No ads or tracking
</h3>
<p className={styles.feature__text}>
Browse any movie info without being tracked or bombarded by
annoying ads.
</p>
</li>
<li className={styles.feature}>
<svg
aria-hidden='true'
focusable='false'
role='img'
className={styles.feature__icon}
>
<use href='/svg/sprite.svg#icon-palette'></use>
</svg>
<h3
className={`heading heading__tertiary ${styles.feature__heading}`}
>
Modern interface
</h3>
<p className={styles.feature__text}>
Modern interface with curated colors supporting both dark and
light themes.
</p>
</li>
<li className={styles.feature}>
<svg
aria-hidden='true'
focusable='false'
role='img'
className={styles.feature__icon}
>
<use href='/svg/sprite.svg#icon-responsive'></use>
</svg>
<h3
className={`heading heading__tertiary ${styles.feature__heading}`}
>
Responsive design
</h3>
<p className={styles.feature__text}>
Be it your small mobile or big computer screen, it's fully
responsive.
</p>
</li>
</ul>
</section>
<section id='faq' className={styles.faqs}>
<h2 className={`heading heading__secondary ${styles.faqs__heading}`}>
Questions you may have
</h2>
<div className={styles.faqs__list}>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
How do I use this?
</summary>
<p className={styles.faq__description}>
Replace `imdb.com` in any IMDb URL with any of the instances.
For example: `
<a
href='https://imdb.com/title/tt1049413'
className='link'
target='_blank'
rel='noreferrer'
>
imdb.com/title/tt1049413
</a>
` to `
<Link href='/title/tt1049413'>
<a className='link'>
{process.env.NEXT_PUBLIC_URL || ''}/title/tt1049413
</a>
</Link>
` . To avoid changing the URLs manually, you can use extensions
like{' '}
<a
href='https://github.com/libredirect/libredirect/'
className='link'
>
LibRedirect
</a>
.
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>Why is it slow?</summary>
<p className={styles.faq__description}>
Whenever you request info about a movie/show on libremdb, 4
trips are made(2 between your browser and libremdb's server, and
2 between libremdb's server and IMDb's server) instead of the
usual 2 trips when you visit a website. For this reason there's
a noticable delay. This is a bit of inconvenience you'll have to
face should you wish to use this website.
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
It doesn't have all routes.
</summary>
<p className={styles.faq__description}>
I'll implement more with time :)
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
Is content served from third-parties, like Amazon?
</summary>
<p className={styles.faq__description}>
Nope, libremdb proxies all image and video requests through the
instance to avoid exposing your IP address, browser information
and other personally identifiable metadata (
<a
href='https://github.com/httpjamesm'
target='_blank'
rel='noopener noreferrer'
className='link'
>
Contributor
</a>
).
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
Why not just use IMDb?
</summary>
<p className={styles.faq__description}>
Refer to the{' '}
<a className='link' href='#features'>
features section
</a>{' '}
above.
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
Why didn't you use other databases like TMDB or OMDb?
</summary>
<p className={styles.faq__description}>
IMDb simply has superior dataset compared to all other
alternatives. With that being said, I'd encourage you to check
out those alternatives too.
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
Your website name is quite, ehm, lame.
</summary>
<p className={styles.faq__description}>
Let's just say I'm not very good at naming things.
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
I have some ideas/features/suggestions.
</summary>
<p className={styles.faq__description}>
That's great! I've a couple of{' '}
<Link href='/contact'>
<a className='link'>contact methods</a>
</Link>
. Send your beautiful suggestions(or complaints), or just drop a
hi.
</p>
</details>
</div>
</section>
</Layout>
</>
);
};
export default About;

View File

@ -1,68 +0,0 @@
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import styles from 'src/styles/modules/pages/contact/contact.module.scss';
const Contact = () => {
return (
<>
<Meta
title='Contact'
description='Contact page of libremdb, a free & open source IMDb front-end.'
/>
<Layout className=''>
<section className={styles.contact}>
<h1 className={`heading heading__primary ${styles.contact__heading}`}>
Contact
</h1>
<div className={styles.list}>
<div className={styles.item}>
<p className={styles.item__text}>
For any issues, questions, bugs, or requests regarding the
service, you can go to{' '}
<a href='https://github.com/zyachel/libremdb' className='link'>
GitHub
</a>
.
</p>
<p className={styles.item__text}>
Alternatively, you can visit{' '}
<a
href='https://codeberg.org/zyachel/libremdb'
className='link'
>
the repository on Codeberg
</a>
.
</p>
</div>
{process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
<div className={styles.item}>
<p className={styles.item__text}>
If you have some questions related to this instance,{' '}
<a
href={process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL}
className='link'
>
contact instance maintainer(s)
</a>
.
</p>
</div>
)}
<div className={styles.item}>
<p className={styles.item__text}>
In case you wish to contact me(the dev) personally,{' '}
<a href='https://iket.me/contact/' className='link'>
here you go
</a>
<span aria-label='smily text emoji'> :)</span>
</p>
</div>
</div>
</section>
</Layout>
</>
);
};
export default Contact;

View File

@ -67,13 +67,13 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
props: { data: { title: query, results: res }, error: null, originalPath },
};
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const err = getErrorProperties(error);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return {
props: {
error: { message, statusCode },
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
data: { title: query, results: null },
originalPath,
},

View File

@ -55,11 +55,17 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const err = getErrorProperties(error);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
return {
props: {
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
data: null,
originalPath,
},
};
}
};

View File

@ -1,89 +0,0 @@
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import packageInfo from 'src/../package.json';
import styles from 'src/styles/modules/pages/privacy/privacy.module.scss';
const Privacy = () => {
return (
<>
<Meta
title='Privacy'
description='Privacy policy of libremdb, a free & open source IMDb front-end.'
/>
<Layout className={styles.privacy}>
<section className={styles.policy}>
<h1 className={`heading heading__primary ${styles.policy__heading}`}>
Privacy Policy
</h1>
<div className={styles.list}>
<section className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
Information collected
</h2>
<p className={styles.item__text}>No information is collected.</p>
</section>
<section className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
Information stored in your browser
</h2>
<p className={styles.item__text}>
A key named 'theme' is stored in Local Storage provided by your
browser, if you ever override the default theme. To remove it,
go to site data settings, and clear the data for this website.
</p>
<p className={styles.item__text}>
To permamently disable libremdb from storing your theme
prefrences, either turn off JavaScript or disable access to
Local Storage for libremdb.
</p>
</section>
<section className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
Instance information
</h2>
{process.env.NEXT_PUBLIC_INSTANCE_NAME &&
process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
<p className={styles.item__text}>
Operated by:&nbsp;
<a
className='link'
href={process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL}
>
{process.env.NEXT_PUBLIC_INSTANCE_NAME}
</a>
</p>
)}
<p className={styles.item__text}>
Version:&nbsp;
<a
className='link'
href={`https://github.com/zyachel/libremdb/tree/v${packageInfo.version}`}
>
{packageInfo.version}
</a>
</p>
</section>
</div>
<footer className={styles.metadata}>
<p>
Privacy policy last updated on <time>31 october, 2022.</time>
</p>
<p>
You can see the full revision history of this privacy policy on
GitHub, or Codeberg.
</p>
</footer>
</section>
</Layout>
</>
);
};
export default Privacy;

View File

@ -5,7 +5,7 @@ import ErrorInfo from 'src/components/error/ErrorInfo';
import Media from 'src/components/media/Media';
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
import Title from 'src/interfaces/shared/title';
import { AppError } from 'src/interfaces/shared/error';
import type { AppError } from 'src/interfaces/shared/error';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import title from 'src/utils/fetchers/title';
import { getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers';
@ -63,12 +63,18 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
} catch (e) {
const err = getErrorProperties(e);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
const error = {
message: err.message,
statusCode: err.statusCode,
stack: err.format(),
};
console.error(err);
return { props: { error, data: null, originalPath } };
}
};

View File

@ -1,72 +0,0 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import { BasicCard, Filters, Pagination, Reviews } from 'src/components/titleReviews';
import { AppError } from 'src/interfaces/shared/error';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { cursoredReviews } from 'src/utils/fetchers/titleReviews';
import { cleanQueryStr, getErrorProperties } from 'src/utils/helpers';
import { titleReviewsKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/titleReviews/titleReviews.module.scss';
import { TitleReviewsCursored } from 'src/interfaces/shared/titleReviews';
import { keys as titleReviewsQueryKeys } from 'src/utils/constants/titleReviewsFilters';
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const CursoredReviewsPage = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
return (
<>
<Meta
title={data.meta.title ?? 'User Reviews'}
description={data.meta.title ?? 'User Reviews'}
/>
<Layout className={styles.container} originalPath={originalPath}>
<BasicCard meta={data.meta} className={styles.card} />
<Reviews list={data.list} className={styles.results}>
<Pagination meta={data.meta} cursor={data.cursor} />
</Reviews>
<Filters titleId={data.meta.titleId} className={styles.form} />
</Layout>
</>
);
};
// TO-DO: make a getServerSideProps wrapper for handling errors
type Data = ({ data: TitleReviewsCursored; error: null } | { error: AppError; data: null }) & {
originalPath: string;
};
type Params = { titleId: string; paginationKey: string };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
const titleId = ctx.params!.titleId;
const paginationKey = ctx.params!.paginationKey;
const title = ctx.query.title as string | null;
const originalPath = `/title/${titleId}/reviews/_ajax?paginationKey=${paginationKey}`;
const queryObj = ctx.query as Record<string, string>;
const queryStr = cleanQueryStr(queryObj, titleReviewsQueryKeys);
try {
const data = await getOrSetApiCache(
titleReviewsKey(titleId, queryStr, paginationKey),
cursoredReviews,
titleId,
paginationKey,
queryStr,
title
);
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
}
};
export default CursoredReviewsPage;

View File

@ -1,92 +1,9 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import { Filters, Pagination, Reviews, TitleCard } from 'src/components/titleReviews';
import { AppError } from 'src/interfaces/shared/error';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import titleReviews from 'src/utils/fetchers/titleReviews';
import { cleanQueryStr, getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers';
import { titleReviewsKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/titleReviews/titleReviews.module.scss';
import TitleReviews from 'src/interfaces/shared/titleReviews';
import { keys as titleReviewFiltersQueryKeys } from 'src/utils/constants/titleReviewsFilters';
import { useState } from 'react';
import ProgressBar from 'src/components/loaders/ProgressBar';
import { useRouter } from 'next/router';
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
// TO-DO: make a wrapper page component to display errors, if present in props
const ReviewsPage = ({ data, error, originalPath }: Props) => {
const [allData, setAllData] = useState({ list: data?.list ?? [], cursor: data?.cursor ?? null });
const [isFetching, setIsFetching] = useState(false);
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
const handleOnClickLoadMore = (queryStr = '') => {
if (!data.cursor) return;
setIsFetching(true);
fetch(`/api/title/${data.meta.titleId}/reviews/${allData.cursor}?${queryStr}`)
.then(res => {
if (!res.ok) throw new Error('something went wrong');
return res.json();
})
.then((newData: { data: NonNullable<Props['data']> }) =>
setAllData(prev => ({
list: prev.list.concat(newData.data.list),
cursor: newData.data.cursor ?? null,
}))
)
.catch(console.log)
.finally(() => setIsFetching(false));
};
return (
<>
{isFetching && <ProgressBar />}
<Meta
title={data.meta.title}
description={data.meta.title}
imgUrl={data.meta?.image ? getProxiedIMDbImgUrl(data.meta.image) : undefined}
/>
<Layout className={styles.container} originalPath={originalPath}>
<TitleCard meta={data.meta} className={styles.card} />
<Reviews list={allData.list} className={styles.results}>
<Pagination meta={data.meta} cursor={allData.cursor} onClick={handleOnClickLoadMore} />
</Reviews>
<Filters titleId={data.meta.titleId} className={styles.form} />
</Layout>
</>
);
};
// TO-DO: make a getServerSideProps wrapper for handling errors
type Data = ({ data: TitleReviews; error: null } | { error: AppError; data: null }) & {
originalPath: string;
};
type Params = { titleId: string };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
const titleId = ctx.params!.titleId;
const originalPath = ctx.resolvedUrl;
const queryParams = ctx.query as Record<string, string>;
const queryStr = cleanQueryStr(queryParams, titleReviewFiltersQueryKeys);
try {
const data = await getOrSetApiCache(
titleReviewsKey(titleId, queryStr, null),
titleReviews,
titleId,
queryStr
);
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
}
const ReviewsPage = () => {
const router = useRouter();
return <ErrorInfo message='Not yet implemented' statusCode={503} originalPath={router.asPath} />;
};
export default ReviewsPage;

View File

@ -1,47 +1,10 @@
$_light: (
// 1. text
// 1.1 for headings
text-accent: hsl(240, 31%, 25%),
// 1.2 for base text
text: hsl(0, 0%, 24%),
// 1.3 for subtle text like metadata
text-muted: hsl(204, 4%, 35%),
// 2. bg
// 2.1 for cards, headers, footers,
bg-accent: hsl(339, 100%, 97%),
// 2.2 for base bg
bg: hsl(0, 0%, 100%),
// 2.3 for hover state of cards
bg-muted: rgb(255, 229, 239),
// 3. links
// 3.1 the default one.
link: hsl(219, 100%, 20%),
link-muted: hsl(344, 79%, 40%),
// 4. for icons, borders
fill: hsl(339, 100%, 36%),
// 4.2 for borders, primarily
fill-muted: hsl(0, 0%, 80%),
// shadows on cards
shadow: 0 0 0.5em hsla(0, 0%, 0%, 0.2),
// keyboard, focus hightlight
highlight: hsl(176, 43%, 46%),
// for gradient behind hero text on about page.
gradient:
(
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.15) 0px, transparent 70%),
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.2) 0px, transparent 50%)
),
// changes color of native html elemnts, either 'light' or 'dark' must be set.
scheme: light
);
$_dark: (
text-accent: hsl(0, 0%, 100%),
text: hsl(0, 0%, 96%),
text-muted: hsl(0, 0%, 80%),
bg-accent: hsl(221, 39%, 15%),
bg: hsl(221, 39%, 11%),
bg-muted: rgb(20, 28, 46),
bg-accent: #232323,
bg: #000,
bg-muted: #141414,
link: hsl(339, 95%, 80%),
link-muted: hsl(344, 79%, 80%),
fill: hsl(339, 75%, 64%),
@ -56,6 +19,6 @@ $_dark: (
);
$themes: (
light: $_light,
light: $_dark, // yes
dark: $_dark,
);

View File

@ -8,7 +8,7 @@
display: grid;
justify-content: center;
justify-items: center;
gap: var(--spacer-1);
gap: var(--comp-whitespace);
@include helper.bp('bp-700') {
--doc-whitespace: var(--spacer-5);
@ -31,6 +31,19 @@
text-align: center;
}
.stack {
max-width: 90%;
max-height: 20rem;
padding: var(--spacer-3);
white-space: pre-wrap;
overflow: scroll;
user-select: all;
border-radius: var(--spacer-1);
background-color: var(--clr-bg-muted);
}
.button {
align-self: end;

View File

@ -1,26 +1,94 @@
.reviews {
display: grid;
gap: var(--comp-whitespace);
}
&__reviewContainer {
// background-color: antiquewhite;
.ratingsDistribution {
overflow: hidden;
ul {
list-style: none;
display: flex;
gap: var(--spacer-2);
overflow-x: auto;
padding-block: var(--spacer-1);
}
&__stats {
li {
text-align: center;
display: flex;
flex-direction: column;
height: 5em;
align-items: center;
flex-shrink: 0;
&::before {
display: block;
content: '';
height: var(--bar-height);
margin-top: auto;
background-color: var(--clr-fill);
width: var(--spacer-6);
border-radius: var(--spacer-0);
}
span {
font-weight: var(--fw-bold);
> span {
font-weight: initial;
font-size: 0.9em;
color: var(--clr-text-muted);
}
}
}
}
.reviewAi {
summary {
cursor: pointer;
}
p {
padding-block: var(--spacer-2);
}
ul {
list-style: none;
display: flex;
gap: var(--spacer-1);
flex-wrap: wrap;
gap: var(--spacer-2);
li {
padding: var(--spacer-1);
background-color: var(--clr-bg-muted);
border-radius: var(--spacer-0);
}
}
}
.reviewStats {
display: flex;
flex-wrap: wrap;
gap: var(--spacer-2);
}
.userReviews {
&__list {
padding-block-start: var(--spacer-1);
display: grid;
gap: var(--spacer-1);
list-style: none;
}
}
.review {
&__summary {
font-size: calc(var(--fs-5) * 1.1);
cursor: pointer;
}
&__text,
&__metadata {
padding-top: var(--spacer-2);
padding-top: var(--spacer-1);
}
}

View File

@ -2,7 +2,7 @@
.footer {
background: var(--clr-bg-muted);
padding: var(--spacer-4);
padding: var(--spacer-3);
display: flex;
flex-direction: column;
@ -15,9 +15,10 @@
list-style: none;
display: flex;
gap: var(--spacer-2) var(--spacer-4);
justify-content: space-evenly;
justify-content: center;
flex-wrap: wrap;
}
&__item {
}

View File

@ -6,7 +6,8 @@
font-size: 1.1em;
display: grid;
background: (var(--clr-bg-muted));
background: none;
border-bottom: solid 1px (var(--clr-text-accent));
&__about {
min-height: 100vh;
@ -24,7 +25,7 @@
align-items: center;
gap: var(--spacer-4);
padding: var(--spacer-4);
padding: var(--spacer-3);
@include helper.bp('bp-700') {
padding: var(--spacer-3);

View File

@ -42,18 +42,18 @@ const cleanName = (rawData: RawName) => {
},
media: {
...(main.primaryVideos.edges.length && {
trailer: {
id: main.primaryVideos.edges[0].node.id,
isMature: main.primaryVideos.edges[0].node.isMature,
thumbnail: main.primaryVideos.edges[0].node.thumbnail.url,
runtime: main.primaryVideos.edges[0].node.runtime.value,
caption: main.primaryVideos.edges[0].node.description?.value ?? null,
urls: main.primaryVideos.edges[0].node.playbackURLs.map(url => ({
resolution: url.displayName.value,
mimeType: url.mimeType ?? null,
trailers: main.primaryVideos.edges.map(trailer => ({
id: trailer.node.id,
isMature: trailer.node.isMature,
thumbnail: trailer.node.thumbnail.url,
runtime: trailer.node.runtime.value,
caption: trailer.node.description?.value ?? null,
urls: trailer.node.playbackURLs.map(url => ({
resolution: url.displayName.value as 'SD' | '480p',
mimeType: url.videoMimeType ?? null,
url: url.url,
})),
},
})),
}),
images: {
total: misc.images.total,

View File

@ -12,6 +12,7 @@ const cleanTitle = (rawData: RawTitle) => {
titleId: main.id,
basic: {
id: main.id,
isAdult: main.isAdult,
title: main.titleText.text,
// ...(main.originalTitleText.text.toLowerCase() !==
// main.titleText.text.toLowerCase() && {
@ -50,6 +51,10 @@ const cleanTitle = (rawData: RawTitle) => {
id: genre.id,
text: genre.text,
})),
interests: main.interests.edges.map(interest => ({
id: interest.node.id,
text: interest.node.primaryText.text,
})),
plot: main.plot?.plotText?.plainText || null,
primaryCrew: main.principalCredits.map(type => ({
type: { category: type.category.text, id: type.category.id },
@ -76,18 +81,18 @@ const cleanTitle = (rawData: RawTitle) => {
})),
media: {
...(main.primaryVideos.edges.length && {
trailer: {
id: main.primaryVideos.edges[0].node.id,
isMature: main.primaryVideos.edges[0].node.isMature,
thumbnail: main.primaryVideos.edges[0].node.thumbnail.url,
runtime: main.primaryVideos.edges[0].node.runtime.value,
caption: main.primaryVideos.edges[0].node.description?.value ?? null,
urls: main.primaryVideos.edges[0].node.playbackURLs.map(url => ({
resolution: url.displayName.value,
mimeType: url.mimeType ?? null,
trailers: main.primaryVideos.edges.map(trailer => ({
id: trailer.node.id,
isMature: trailer.node.isMature,
thumbnail: trailer.node.thumbnail.url,
runtime: trailer.node.runtime.value,
caption: trailer.node.description?.value ?? null,
urls: trailer.node.playbackURLs.map(url => ({
resolution: url.displayName.value as 'SD' | '480p',
mimeType: url.videoMimeType ?? null,
url: url.url,
})),
},
})),
}),
images: {
total: misc.titleMainImages.total,
@ -122,6 +127,9 @@ const cleanTitle = (rawData: RawTitle) => {
}),
topRating: misc.ratingsSummary.topRanking?.rank || null,
},
watchlist: {
text: main.engagementStatistics?.watchlistStatistics.displayableCount.text || null,
},
meta: {
// for tv episode
...(main.series && {
@ -208,25 +216,35 @@ const cleanTitle = (rawData: RawTitle) => {
metacriticScore: main.metacritic?.metascore.score || null,
numCriticReviews: main.criticReviewsTotal.total,
numUserReviews: misc.reviews.total,
...(misc.featuredReviews.edges.length && {
featuredReview: {
id: misc.featuredReviews.edges[0].node.id,
reviewer: {
id: misc.featuredReviews.edges[0].node.author.userId,
name: misc.featuredReviews.edges[0].node.author.nickName,
},
rating: misc.featuredReviews.edges[0].node.authorRating,
date: formatDate(misc.featuredReviews.edges[0].node.submissionDate),
votes: {
up: misc.featuredReviews.edges[0].node.helpfulness.upVotes,
down: misc.featuredReviews.edges[0].node.helpfulness.downVotes,
},
review: {
summary: misc.featuredReviews.edges[0].node.summary.originalText,
html: misc.featuredReviews.edges[0].node.text.originalText.plaidHtml,
},
ratingsDistribution:
misc.aggregateRatingsBreakdown.histogram?.histogramValues.map(v => ({
rating: v.rating,
votes: v.voteCount,
})) || [],
...(misc.reviewSummary && {
ai: {
summary: misc.reviewSummary.overall.medium.value.plaidHtml,
themes: misc.reviewSummary.themes.map(t => ({
text: t.label.value,
id: t.themeId,
sentiment: t.sentiment as 'POSITIVE' | 'NEGATIVE',
})),
},
}),
...(misc.featuredReviews.edges.length && {
featuredReviews: misc.featuredReviews.edges.map(featuredReview => ({
id: featuredReview.node.id,
reviewer: {
id: featuredReview.node.author.userId,
name: featuredReview.node.author.username.text,
},
rating: featuredReview.node.authorRating,
review: {
summary: featuredReview.node.summary.originalText,
html: featuredReview.node.text.originalText.plaidHtml,
},
})),
}),
},
details: {
...(misc.releaseDate && {
@ -242,8 +260,8 @@ const cleanTitle = (rawData: RawTitle) => {
},
},
}),
...(misc.countriesOfOrigin && {
countriesOfOrigin: misc.countriesOfOrigin.countries.map(country => ({
...(misc.countriesDetails && {
countriesOfOrigin: misc.countriesDetails.countries.map(country => ({
id: country.id,
text: country.text,
})),
@ -353,6 +371,10 @@ const cleanTitle = (rawData: RawTitle) => {
},
genres: title.node.titleGenres?.genres.map(genre => genre.genre.text) ?? null,
})),
faqs: {
questions: misc.faqs.edges.map(q => ({ question: q.node.question, id: q.node.id })),
total: misc.faqs.total,
},
};
return cleanData;

View File

@ -74,12 +74,33 @@ export const getProxiedIMDbImgUrl = (url: string) => {
};
export const AppError = class extends Error {
constructor(message: string, public statusCode: number, errorOptions?: unknown) {
const saneErrorOptions = getErrorOptions(errorOptions);
super(message, saneErrorOptions);
constructor(message: string, public statusCode: number, cause?: unknown) {
const _cause = cause ? AppError.toError(cause) : undefined;
super(message, { cause: _cause });
Error.captureStackTrace(this, AppError);
if (process.env.NODE_ENV === 'development') console.error(this);
}
static toError(err: unknown) {
if (err instanceof Error) return err;
return new Error(`Unexpected: ${JSON.stringify(err)}`);
}
format() {
let str = '';
let cur: Error | null = this;
let depth = 0;
while (cur && depth <= 4) {
if (cur.stack) str += `${cur.stack}\n`;
else str += `${cur.name}: ${cur.message}\n`;
cur = cur.cause instanceof Error ? cur.cause : null;
if (cur) str += 'Caused by:\n';
depth++;
}
return str.trimEnd();
}
};
@ -110,19 +131,6 @@ export const isLocalStorageAvailable = () => {
}
};
const getErrorOptions = (error: unknown): ErrorOptions | undefined => {
if (!error || typeof error !== 'object') return undefined;
let cause: unknown;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
if ('cause' in error) cause = error.cause;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
else if ('stack' in error) cause = error.stack;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
return { cause };
};
export const getErrorProperties = (
error: unknown,
message = 'Something went very wrong',
@ -130,4 +138,4 @@ export const getErrorProperties = (
) => {
if (error instanceof AppError) return error;
return new AppError(message, statusCode, error);
};
};

5
ups.json Normal file
View File

@ -0,0 +1,5 @@
{
"upstream": "https://github.com/zyachel/libremdb",
"provider": "github",
"commit": "dded1e59ca6df40ccb1b3f2c70de14144afbe922"
}