Compare commits
1 Commits
preview-em
...
preview-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0504603809 |
126
.github/workflows/ci.yml
vendored
126
.github/workflows/ci.yml
vendored
@@ -7,6 +7,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -44,111 +52,109 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
build:
|
||||
name: Build & Publish Docker Images
|
||||
name: Build (per-arch, native runners)
|
||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux/amd64
|
||||
arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
outputs:
|
||||
digest-amd64: ${{ steps.set_outputs.outputs.digest-amd64 }}
|
||||
digest-arm64: ${{ steps.set_outputs.outputs.digest-arm64 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deterministic timestamp
|
||||
id: ts
|
||||
run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set lower case owner name
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
fallenbagel/jellyseerr
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=,suffix=,format=short
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
|
||||
- name: Warm cache (no push) — ${{ matrix.platform }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
push: false
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
BUILD_VERSION=develop
|
||||
BUILD_DATE=${{ github.event.repository.updated_at }}
|
||||
outputs: |
|
||||
type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true
|
||||
type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true
|
||||
BUILD_DATE=${{ steps.ts.outputs.now }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
provenance: false
|
||||
- name: Set outputs
|
||||
id: set_outputs
|
||||
run: |
|
||||
platform="${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}"
|
||||
echo "digest-${platform}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
|
||||
|
||||
merge_and_push:
|
||||
name: Create and Push Multi-arch Manifest
|
||||
publish:
|
||||
name: Publish multi-arch image
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deterministic timestamp
|
||||
id: ts
|
||||
run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU (enable ARM emulation)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set lower case owner name
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
|
||||
- name: Lower-case owner
|
||||
run: echo "OWNER_LC=${OWNER,,}" >> "$GITHUB_ENV"
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
docker manifest create fallenbagel/jellyseerr:develop \
|
||||
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
|
||||
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
|
||||
docker manifest push fallenbagel/jellyseerr:develop
|
||||
|
||||
# GHCR manifest
|
||||
docker manifest create ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop \
|
||||
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
|
||||
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
|
||||
docker manifest push ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||
- name: Build & Push (multi-arch, single tag)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
BUILD_VERSION=develop
|
||||
BUILD_DATE=${{ steps.ts.outputs.now }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.version=develop
|
||||
tags: |
|
||||
fallenbagel/jellyseerr:develop
|
||||
fallenbagel/jellyseerr:${{ github.sha }}
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:${{ github.sha }}
|
||||
cache-from: |
|
||||
type=gha,scope=linux/amd64
|
||||
type=gha,scope=linux/arm64
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: merge_and_push
|
||||
needs: publish
|
||||
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
|
||||
121
.github/workflows/preview.yml
vendored
121
.github/workflows/preview.yml
vendored
@@ -4,28 +4,113 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'preview-*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: preview-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
name: Build & Publish Docker Preview Images
|
||||
runs-on: ubuntu-22.04
|
||||
build:
|
||||
name: Build (per-arch, native runners)
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux/amd64
|
||||
arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Deterministic timestamp
|
||||
id: ts
|
||||
run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Derive preview version from tag
|
||||
id: ver
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
VER="${TAG#preview-}"
|
||||
VER="${VER#v}"
|
||||
echo "version=${VER}" >> "$GITHUB_OUTPUT"
|
||||
echo "Building preview version: ${VER}"
|
||||
|
||||
- name: Warm cache (no push) — ${{ matrix.platform }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: false
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
BUILD_VERSION=${{ steps.ver.outputs.version }}
|
||||
BUILD_DATE=${{ steps.ts.outputs.now }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
provenance: false
|
||||
|
||||
publish:
|
||||
name: Publish multi-arch image
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deterministic timestamp
|
||||
id: ts
|
||||
run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU (enable ARM emulation)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Lower-case owner
|
||||
run: echo "OWNER_LC=${OWNER,,}" >> "$GITHUB_ENV"
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
|
||||
- name: Derive preview version from tag
|
||||
id: ver
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
VER="${TAG#preview-}"
|
||||
VER="${VER#v}"
|
||||
echo "version=${VER}" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing preview version: ${VER}"
|
||||
|
||||
- name: Build & Push (multi-arch, single tag)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -33,7 +118,17 @@ jobs:
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
BUILD_VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||
BUILD_DATE=${{ github.event.repository.updated_at }}
|
||||
BUILD_VERSION=${{ steps.ver.outputs.version }}
|
||||
BUILD_DATE=${{ steps.ts.outputs.now }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.version=preview-${{ steps.ver.outputs.version }}
|
||||
tags: |
|
||||
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}
|
||||
fallenbagel/jellyseerr:preview-${{ steps.ver.outputs.version }}
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:preview-${{ steps.ver.outputs.version }}
|
||||
cache-from: |
|
||||
type=gha,scope=linux/amd64
|
||||
type=gha,scope=linux/arm64
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
|
||||
212
.github/workflows/release.yml
vendored
212
.github/workflows/release.yml
vendored
@@ -1,6 +1,15 @@
|
||||
name: Jellyseer Release
|
||||
name: Jellyseerr Release
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
semantic-release:
|
||||
@@ -8,38 +17,30 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
HUSKY: 0
|
||||
outputs:
|
||||
new_release_published: ${{ steps.release.outputs.new_release_published }}
|
||||
new_release_version: ${{ steps.release.outputs.new_release_version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Pnpm Setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: sh
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -47,74 +48,135 @@ jobs:
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Release
|
||||
id: release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: npx semantic-release
|
||||
|
||||
# build-snap:
|
||||
# name: Build Snap Package (${{ matrix.architecture }})
|
||||
# needs: semantic-release
|
||||
# runs-on: ubuntu-22.04
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# architecture:
|
||||
# - amd64
|
||||
# - arm64
|
||||
# steps:
|
||||
# - name: Checkout Code
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: Switch to main branch
|
||||
# run: git checkout main
|
||||
# - name: Pull latest changes
|
||||
# run: git pull
|
||||
# - name: Prepare
|
||||
# id: prepare
|
||||
# run: |
|
||||
# git fetch --prune --tags
|
||||
# if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
# echo "RELEASE=stable" >> $GITHUB_OUTPUT
|
||||
# else
|
||||
# echo "RELEASE=edge" >> $GITHUB_OUTPUT
|
||||
# fi
|
||||
# - name: Set Up QEMU
|
||||
# uses: docker/setup-qemu-action@v3
|
||||
# with:
|
||||
# image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||
# - name: Build Snap Package
|
||||
# uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
# id: build
|
||||
# with:
|
||||
# architecture: ${{ matrix.architecture }}
|
||||
# - name: Upload Snap Package
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||
# path: ${{ steps.build.outputs.snap }}
|
||||
# - name: Review Snap Package
|
||||
# uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
# with:
|
||||
# snap: ${{ steps.build.outputs.snap }}
|
||||
# - name: Publish Snap Package
|
||||
# uses: snapcore/action-publish@v1
|
||||
# env:
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }}
|
||||
# with:
|
||||
# snap: ${{ steps.build.outputs.snap }}
|
||||
# release: ${{ steps.prepare.outputs.RELEASE }}
|
||||
build:
|
||||
name: Build (per-arch, native runners)
|
||||
needs: semantic-release
|
||||
if: needs.semantic-release.outputs.new_release_published == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux/amd64
|
||||
arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deterministic timestamp
|
||||
id: ts
|
||||
run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Warm cache (no push) — ${{ matrix.platform }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: false
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
BUILD_VERSION=${{ needs.semantic-release.outputs.new_release_version }}
|
||||
BUILD_DATE=${{ steps.ts.outputs.now }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
provenance: false
|
||||
|
||||
publish:
|
||||
name: Publish multi-arch image
|
||||
needs: [semantic-release, build]
|
||||
if: needs.semantic-release.outputs.new_release_published == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deterministic timestamp
|
||||
id: ts
|
||||
run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU (enable ARM emulation)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Lower-case owner
|
||||
run: echo "OWNER_LC=${OWNER,,}" >> "$GITHUB_ENV"
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
|
||||
- name: Build & Push (multi-arch, single tag)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
BUILD_VERSION=${{ needs.semantic-release.outputs.new_release_version }}
|
||||
BUILD_DATE=${{ steps.ts.outputs.now }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.version=${{ needs.semantic-release.outputs.new_release_version }}
|
||||
tags: |
|
||||
fallenbagel/jellyseerr:${{ needs.semantic-release.outputs.new_release_version }}
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:${{ needs.semantic-release.outputs.new_release_version }}
|
||||
cache-from: |
|
||||
type=gha,scope=linux/amd64
|
||||
type=gha,scope=linux/arm64
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
|
||||
- name: Also tag :latest (non-pre-release only)
|
||||
shell: bash
|
||||
run: |
|
||||
VER="${{ needs.semantic-release.outputs.new_release_version }}"
|
||||
if [[ "$VER" != *"-"* ]]; then
|
||||
docker buildx imagetools create \
|
||||
-t fallenbagel/jellyseerr:latest \
|
||||
fallenbagel/jellyseerr:${VER}
|
||||
docker buildx imagetools create \
|
||||
-t ghcr.io/${{ env.OWNER_LC }}/jellyseerr:latest \
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:${VER}
|
||||
fi
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: semantic-release
|
||||
needs: publish
|
||||
if: always()
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -71,6 +71,3 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# Config Cache Directory
|
||||
config/cache
|
||||
|
||||
# Mise
|
||||
mise.toml
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { uniqueId } from 'lodash';
|
||||
import type { JellyfinLoginResponse } from './jellyfin';
|
||||
|
||||
export interface ConnectAuthResponse {
|
||||
AccessToken: string;
|
||||
User: {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Email: string;
|
||||
IsActive: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LinkedServer {
|
||||
Id: string;
|
||||
Url: string;
|
||||
Name: string;
|
||||
SystemId: string;
|
||||
AccessKey: string;
|
||||
LocalAddress: string;
|
||||
UserType: string;
|
||||
SupporterKey: string;
|
||||
}
|
||||
|
||||
export interface LocalUserAuthExchangeResponse {
|
||||
LocalUserId: string;
|
||||
AccessToken: string;
|
||||
}
|
||||
|
||||
export interface EmbyConnectOptions {
|
||||
ClientIP?: string;
|
||||
DeviceId?: string;
|
||||
}
|
||||
|
||||
const EMBY_CONNECT_URL = 'https://connect.emby.media';
|
||||
|
||||
class EmbyConnectAPI extends ExternalAPI {
|
||||
private ClientIP?: string;
|
||||
private DeviceId?: string;
|
||||
|
||||
constructor(options: EmbyConnectOptions = {}) {
|
||||
super(
|
||||
EMBY_CONNECT_URL,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'X-Application': `Jellyseerr/${getAppVersion()}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(getSettings().main.mediaServerType === MediaServerType.EMBY &&
|
||||
{}),
|
||||
},
|
||||
}
|
||||
);
|
||||
this.ClientIP = options.ClientIP;
|
||||
this.DeviceId = options.DeviceId;
|
||||
}
|
||||
|
||||
public async authenticateConnectUser(Email?: string, Password?: string) {
|
||||
logger.debug(`Attempting to authenticate via EmbyConnect with email:`, {
|
||||
Email,
|
||||
});
|
||||
|
||||
const connectAuthResponse = await this.getConnectUserAccessToken(
|
||||
Email,
|
||||
Password
|
||||
);
|
||||
|
||||
const linkedServers = await this.getValidServers(
|
||||
connectAuthResponse.User.Id,
|
||||
connectAuthResponse.AccessToken
|
||||
);
|
||||
|
||||
const matchingServer = this.findMatchingServer(linkedServers);
|
||||
|
||||
const localUserExchangeResponse = await this.localAuthExchange(
|
||||
matchingServer.AccessKey,
|
||||
connectAuthResponse.User.Id,
|
||||
this.DeviceId
|
||||
);
|
||||
|
||||
return {
|
||||
User: {
|
||||
Name: connectAuthResponse.User.Name,
|
||||
Email: connectAuthResponse.User.Email,
|
||||
ServerId: matchingServer.SystemId,
|
||||
ServerName: matchingServer.Name,
|
||||
Id: localUserExchangeResponse.LocalUserId,
|
||||
Configuration: {
|
||||
GroupedFolders: [],
|
||||
},
|
||||
Policy: {
|
||||
IsAdministrator: false, // This requires an additional EmbyServer API call, skipping for now
|
||||
},
|
||||
},
|
||||
AccessToken: localUserExchangeResponse.AccessToken,
|
||||
} as JellyfinLoginResponse;
|
||||
}
|
||||
|
||||
private async getConnectUserAccessToken(
|
||||
Email?: string,
|
||||
Password?: string
|
||||
): Promise<ConnectAuthResponse> {
|
||||
try {
|
||||
const response = await this.post<ConnectAuthResponse>(
|
||||
'/service/user/authenticate',
|
||||
{ nameOrEmail: Email, rawpw: Password },
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
logger.debug(`Failed to authenticate using EmbyConnect:`, {
|
||||
label: 'EmbyConnect API',
|
||||
ip: this.ClientIP,
|
||||
error: e.message,
|
||||
});
|
||||
throw new ApiError(
|
||||
e.cause?.status ?? 401,
|
||||
ApiErrorCode.InvalidCredentials
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getValidServers(
|
||||
ConnectUserId: string,
|
||||
AccessToken: string
|
||||
): Promise<LinkedServer[]> {
|
||||
try {
|
||||
const response = await this.get<LinkedServer[]>(`/service/servers`, {
|
||||
params: { userId: ConnectUserId },
|
||||
headers: {
|
||||
'X-Connect-UserToken': AccessToken,
|
||||
},
|
||||
});
|
||||
return response;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to retrieve EmbyConnect user server list: `, {
|
||||
label: 'EmbyConnect API',
|
||||
ip: this.ClientIP,
|
||||
error: e.message,
|
||||
});
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
private findMatchingServer(linkedEmbyServers: LinkedServer[]): LinkedServer {
|
||||
const settings = getSettings();
|
||||
const matchingServer = linkedEmbyServers.find(
|
||||
(server) => server.SystemId === settings.jellyfin.serverId
|
||||
);
|
||||
|
||||
if (!matchingServer) {
|
||||
throw new Error(
|
||||
`No matching linked Emby server found for serverId: ${settings.jellyfin.serverId}`
|
||||
);
|
||||
}
|
||||
|
||||
return matchingServer;
|
||||
}
|
||||
|
||||
private async localAuthExchange(
|
||||
accessKey: string,
|
||||
userId: string,
|
||||
deviceId?: string
|
||||
): Promise<LocalUserAuthExchangeResponse> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
format: 'json',
|
||||
ConnectUserId: userId,
|
||||
'X-Emby-Client': 'Jellyseerr',
|
||||
'X-Emby-Device-Id': deviceId ?? uniqueId(),
|
||||
'X-Emby-Client-Version': getAppVersion(),
|
||||
'X-Emby-Device-Name': 'Jellyseerr',
|
||||
'X-Emby-Token': accessKey,
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${getHostname()}/emby/Connect/Exchange?${params}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText, { cause: response });
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
logger.debug('Failed local user auth exchange', e.cause);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EmbyConnectAPI;
|
||||
@@ -3,7 +3,6 @@ import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import rateLimit from 'axios-rate-limit';
|
||||
import type NodeCache from 'node-cache';
|
||||
import querystring from 'querystring';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
const DEFAULT_TTL = 300;
|
||||
@@ -83,7 +82,6 @@ class ExternalAPI {
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: config?.params,
|
||||
headers: config?.headers,
|
||||
...(data ? { data } : {}),
|
||||
});
|
||||
|
||||
@@ -92,16 +90,7 @@ class ExternalAPI {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const isFormUrlEncoded = (
|
||||
config?.headers?.['Content-Type'] as string
|
||||
)?.includes('application/x-www-form-urlencoded');
|
||||
|
||||
const body =
|
||||
data && isFormUrlEncoded
|
||||
? querystring.stringify(data as Record<string, string>)
|
||||
: data;
|
||||
|
||||
const response = await this.axios.post<T>(endpoint, body, config);
|
||||
const response = await this.axios.post<T>(endpoint, data, config);
|
||||
|
||||
if (this.cache && ttl !== 0) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import EmbyConnectAPI from '@server/api/embyconnect';
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
@@ -8,11 +7,9 @@ import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
|
||||
export interface JellyfinUserResponse {
|
||||
Name: string;
|
||||
Email?: string;
|
||||
ServerId: string;
|
||||
ServerName: string;
|
||||
Id: string;
|
||||
@@ -122,7 +119,6 @@ export interface JellyfinItemsReponse {
|
||||
|
||||
class JellyfinAPI extends ExternalAPI {
|
||||
private userId?: string;
|
||||
private deviceId?: string;
|
||||
private mediaServerType: MediaServerType;
|
||||
|
||||
constructor(
|
||||
@@ -154,7 +150,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
},
|
||||
}
|
||||
);
|
||||
this.deviceId = deviceId ? deviceId : undefined;
|
||||
|
||||
this.mediaServerType = settings.main.mediaServerType;
|
||||
}
|
||||
|
||||
@@ -177,31 +173,6 @@ class JellyfinAPI extends ExternalAPI {
|
||||
);
|
||||
};
|
||||
|
||||
if (
|
||||
getSettings().main.mediaServerType === MediaServerType.EMBY &&
|
||||
Username &&
|
||||
EmailValidator.validate(Username)
|
||||
) {
|
||||
try {
|
||||
const connectApi = new EmbyConnectAPI({
|
||||
ClientIP: ClientIP,
|
||||
DeviceId: this.deviceId,
|
||||
});
|
||||
return await connectApi.authenticateConnectUser(Username, Password);
|
||||
} catch (e) {
|
||||
// Possible local Emby user with email as username
|
||||
logger.warn(
|
||||
`Emby Connect authentication failed: ${e}, attempting local Emby server authentication`,
|
||||
{
|
||||
label: 'Jellyfin API',
|
||||
error:
|
||||
e.cause?.message ?? e.cause?.statusText ?? ApiErrorCode.Unknown,
|
||||
ip: ClientIP,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await authenticate(true);
|
||||
} catch (e) {
|
||||
@@ -274,9 +245,9 @@ class JellyfinAPI extends ExternalAPI {
|
||||
|
||||
public async getUsers(): Promise<JellyfinUserListResponse> {
|
||||
try {
|
||||
const userResponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
||||
const userReponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
||||
|
||||
return { users: userResponse };
|
||||
return { users: userReponse };
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
@@ -289,10 +260,10 @@ class JellyfinAPI extends ExternalAPI {
|
||||
|
||||
public async getUser(): Promise<JellyfinUserResponse> {
|
||||
try {
|
||||
const userResponse = await this.get<JellyfinUserResponse>(
|
||||
const userReponse = await this.get<JellyfinUserResponse>(
|
||||
`/Users/${this.userId ?? 'Me'}`
|
||||
);
|
||||
return userResponse;
|
||||
return userReponse;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
|
||||
@@ -42,6 +42,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
||||
user.warnings.push('userEmailRequired');
|
||||
logger.warn(`User ${user.username} has no valid email address`);
|
||||
}
|
||||
|
||||
return res.status(200).json(user);
|
||||
});
|
||||
|
||||
@@ -415,34 +416,25 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}
|
||||
// User already exists, let's update their information
|
||||
else if (account.User.Id === user?.jellyfinUserId) {
|
||||
const serverType =
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? ServerType.JELLYFIN
|
||||
: ServerType.EMBY;
|
||||
|
||||
const userType =
|
||||
serverType === ServerType.JELLYFIN ? UserType.JELLYFIN : UserType.EMBY;
|
||||
|
||||
logger.info(
|
||||
`Found matching ${serverType} user; updating user with ${serverType}`,
|
||||
`Found matching ${
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? ServerType.JELLYFIN
|
||||
: ServerType.EMBY
|
||||
} user; updating user with ${
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? ServerType.JELLYFIN
|
||||
: ServerType.EMBY
|
||||
}`,
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
user.userType = userType;
|
||||
user.avatar = getUserAvatarUrl(user);
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
|
||||
if (
|
||||
account.User.Email !== undefined &&
|
||||
user.email !== account.User.Email
|
||||
) {
|
||||
user.email = account.User.Email;
|
||||
}
|
||||
|
||||
if (user.username === account.User.Name) {
|
||||
user.username = '';
|
||||
}
|
||||
@@ -463,59 +455,34 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
message: 'Access denied.',
|
||||
});
|
||||
} else if (!user) {
|
||||
// Handle Emby Connect user with unlinked local account
|
||||
if (
|
||||
settings.main.mediaServerType === MediaServerType.EMBY &&
|
||||
account.User.Email &&
|
||||
account.User.Email.trim() !== ''
|
||||
) {
|
||||
user = await userRepository.findOne({
|
||||
where: { email: account.User.Email },
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
logger.info(
|
||||
`Sign in attempt from EmbyConnect user with access to the media server, linking users`
|
||||
);
|
||||
user.avatar = getUserAvatarUrl(user);
|
||||
user.jellyfinUserId = account.User.Id;
|
||||
user.userType = UserType.EMBY;
|
||||
user.username = account.User.Name;
|
||||
await userRepository.save(user);
|
||||
|
||||
// No user, create new
|
||||
} else {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin/Emby user with access to the media server; creating new Jellyseerr user',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
user = new User({
|
||||
email: body.email,
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating new Jellyseerr user',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
userType:
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
user.avatar = getUserAvatarUrl(user);
|
||||
|
||||
//initialize Jellyfin/Emby users with local login
|
||||
const passedExplicitPassword =
|
||||
body.password && body.password.length > 0;
|
||||
if (passedExplicitPassword) {
|
||||
await user.setPassword(body.password ?? '');
|
||||
}
|
||||
await userRepository.save(user);
|
||||
);
|
||||
|
||||
user = new User({
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
userType:
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
user.avatar = getUserAvatarUrl(user);
|
||||
|
||||
//initialize Jellyfin/Emby users with local login
|
||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||
if (passedExplicitPassword) {
|
||||
await user.setPassword(body.password ?? '');
|
||||
}
|
||||
await userRepository.save(user);
|
||||
}
|
||||
|
||||
if (user && user.jellyfinUserId) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as Yup from 'yup';
|
||||
const messages = defineMessages('components.Login', {
|
||||
loginwithapp: 'Login with {appName}',
|
||||
username: 'Username',
|
||||
email: 'Email Address',
|
||||
password: 'Password',
|
||||
validationusernamerequired: 'Username required',
|
||||
validationpasswordrequired: 'Password required',
|
||||
@@ -126,9 +125,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={`${intl.formatMessage(
|
||||
messages.email
|
||||
)} / ${intl.formatMessage(messages.username)}`}
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||
data-form-type="username"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user