---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: fafflix
  namespace: fafflix
spec:
  # HA: two replicas coordinate transcode session ownership through Valkey and
  # resume each other's HLS segments off the shared RWX transcode PVC. Stable
  # pod names (fafflix-0/1) are the lease owner identity, hence StatefulSet.
  replicas: 2
  serviceName: fafflix
  podManagementPolicy: Parallel
  updateStrategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: fafflix
  template:
    metadata:
      labels:
        app: fafflix
    spec:
      securityContext:
        # Group-write the shared RWX volumes and grant the render/video groups so
        # the runAsUser 1000 process can open the Intel DRI render node injected
        # by the device plugin.
        fsGroup: 1000
        supplementalGroups:
          - 44
          - 105
          - 109
        seccompProfile:
          type: RuntimeDefault
      affinity:
        # Spread the two replicas across nodes for node-level HA. Soft so a
        # single-GPU-node cluster still schedules both (i915 has 4 shared slots).
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: fafflix
                topologyKey: kubernetes.io/hostname
      initContainers:
        # Seed the fork's PostgreSQL provider (database.xml) and Intel iGPU
        # hardware transcode settings (encoding.xml) before Jellyfin starts.
        # Runs as root to chown into the shared config volume; mirrors the fork
        # Helm chart's inject-db-config. Each file is written only when absent so
        # admin changes persisted to the shared RWX /config survive pod restarts.
        - name: inject-config
          image: busybox:1.37.0
          command:
            - sh
            - -c
            - |
              mkdir -p /config/config
              chown 1000:1000 /config/config
              chmod 775 /config/config
              if [ ! -e /config/config/database.xml ]; then
                cat > /config/config/database.xml << 'DBEOF'
              <?xml version="1.0" encoding="utf-8"?>
              <DatabaseConfigurationOptions>
                <DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
                <LockingBehavior>NoLock</LockingBehavior>
              </DatabaseConfigurationOptions>
              DBEOF
                chown 1000:1000 /config/config/database.xml
                chmod 664 /config/config/database.xml
              fi
              # VAAPI on the Intel render node the device plugin injects
              # (/dev/dri/renderD128 — ffmpeg's default DRM node, reachable via
              # the render/video supplementalGroups). Without this the attached
              # iGPU is idle and every transcode runs in software. Omitted
              # elements fall back to the fork's EncodingOptions defaults.
              if [ ! -e /config/config/encoding.xml ]; then
                cat > /config/config/encoding.xml << 'ENCEOF'
              <?xml version="1.0" encoding="utf-8"?>
              <EncodingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
                <EncodingThreadCount>-1</EncodingThreadCount>
                <HardwareAccelerationType>vaapi</HardwareAccelerationType>
                <VaapiDevice>/dev/dri/renderD128</VaapiDevice>
                <EnableHardwareEncoding>true</EnableHardwareEncoding>
                <AllowHevcEncoding>true</AllowHevcEncoding>
                <AllowAv1Encoding>false</AllowAv1Encoding>
                <EnableIntelLowPowerH264HwEncoder>false</EnableIntelLowPowerH264HwEncoder>
                <EnableIntelLowPowerHevcHwEncoder>false</EnableIntelLowPowerHevcHwEncoder>
                <EnableTonemapping>false</EnableTonemapping>
                <EnableVppTonemapping>false</EnableVppTonemapping>
                <HardwareDecodingCodecs>
                  <string>h264</string>
                  <string>hevc</string>
                  <string>vc1</string>
                  <string>vp9</string>
                </HardwareDecodingCodecs>
              </EncodingOptions>
              ENCEOF
                chown 1000:1000 /config/config/encoding.xml
                chmod 664 /config/config/encoding.xml
              fi
          resources:
            requests:
              cpu: 10m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          volumeMounts:
            - name: config
              mountPath: /config
        # Render the SSO/LDAP plugin configs into the shared config volume,
        # substituting the client secret and LDAP bind password from the
        # VSO-synced oauth-credentials Secret (never committed). Plugin configs
        # are fully managed here so they are overwritten every start; the login
        # button branding is written only when absent so admin edits survive.
        - name: inject-plugin-config
          image: busybox:1.37.0
          command:
            - sh
            - -c
            - |
              mkdir -p /config/plugins/configurations /config/config
              chown 1000:1000 /config/plugins /config/plugins/configurations /config/config
              esc() { printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'; }
              cs=$(esc "${CLIENT_SECRET}")
              lp=$(esc "${LDAP_BIND_PASSWORD}")
              sed "s|@@CLIENT_SECRET@@|${cs}|" /templates/SSO-Auth.xml > /config/plugins/configurations/SSO-Auth.xml
              sed "s|@@LDAP_BIND_PASSWORD@@|${lp}|" /templates/LDAP-Auth.xml > /config/plugins/configurations/LDAP-Auth.xml
              chown 1000:1000 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml
              chmod 600 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml
              if [ ! -e /config/config/branding.xml ]; then
                cp /templates/branding.xml /config/config/branding.xml
                chown 1000:1000 /config/config/branding.xml
                chmod 664 /config/config/branding.xml
              fi
          env:
            - name: CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: oauth-credentials
                  key: client_secret
                  optional: true
            - name: LDAP_BIND_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: oauth-credentials
                  key: ldap_bind_password
                  optional: true
          resources:
            requests:
              cpu: 10m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          volumeMounts:
            - name: config
              mountPath: /config
            - name: plugin-config
              mountPath: /templates
              readOnly: true
      containers:
        - name: fafflix
          image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.2.0
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8096
              protocol: TCP
          env:
            # Pod identity for the Valkey transcode lease owner. The fork reads
            # JELLYFIN_INSTANCE_ID (falling back to MachineName); the stable
            # StatefulSet pod name gives each replica a unique lease identity so
            # takeover can target a dead replica. JELLYFIN_HA_POD_NAME is set for
            # parity with the fork Helm chart (nothing currently reads it).
            - name: JELLYFIN_INSTANCE_ID
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: JELLYFIN_HA_POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            # Multiple replicas must not each answer UDP auto-discovery.
            - name: JELLYFIN_Network__AutoDiscovery
              value: "false"
            # Config dir must differ from the data root (Jellyfin sanity check).
            - name: JELLYFIN_CONFIG_DIR
              value: /config/config
            # Distributed transcode session store (fafflix-ha additions).
            - name: Jellyfin__TranscodeStore__RedisConnectionString
              value: "valkey-fafflix-valkey:6379,abortConnect=false"
            - name: Jellyfin__TranscodeStore__LeaseDurationSeconds
              value: "30"
            # PostgreSQL main DB via the CNPG-generated app secret, routed through
            # the PgBouncer pooler. Composed with $(VAR) expansion so the password
            # is never rendered into the manifest; CNPG passwords are URL-safe.
            - name: PGUSER
              valueFrom:
                secretKeyRef:
                  name: fafflix-postgres-app
                  key: username
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: fafflix-postgres-app
                  key: password
            - name: PGDB
              valueFrom:
                secretKeyRef:
                  name: fafflix-postgres-app
                  key: dbname
            - name: POSTGRES_CONNECTION_STRING
              value: "postgresql://$(PGUSER):$(PGPASSWORD)@fafflix-postgres-pooler:5432/$(PGDB)"
            - name: DATABASE_URL
              value: "postgresql://$(PGUSER):$(PGPASSWORD)@fafflix-postgres-pooler:5432/$(PGDB)"
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          resources:
            requests:
              cpu: "1"
              memory: 1Gi
              gpu.intel.com/i915: "1"
            limits:
              cpu: "4"
              memory: 6Gi
              # Intel iGPU (QSV/VA-API) slot. Requesting it pins the pod to a
              # GPU-labelled node and injects /dev/dri/renderD* automatically, so
              # no /dev/dri hostPath or privileged container is needed. VA-API is
              # pre-enabled via the seeded encoding.xml (see inject-config), so
              # transcodes use the iGPU on first boot with no manual UI step.
              gpu.intel.com/i915: "1"
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
          volumeMounts:
            - name: config
              mountPath: /config
            - name: transcode
              # Fork's real transcode temp path. RWX so a surviving pod reads the
              # in-flight .ts/.m3u8 segments of the pod it takes over. A per-pod
              # volume here silently breaks HA takeover.
              mountPath: /config/transcodes
            - name: cache
              mountPath: /cache
            - name: media-tv
              # Adult instance: mount the tvshows/adult subtree of the shared TV
              # subvolume (subPath adult) as fafflix's primary TV library. Same
              # rootPath the cheeztv instance reads under subPath kids, so an
              # episode resolves identically across instances.
              mountPath: /media/tv
              subPath: adult
              readOnly: true
            - name: media-tv
              # Also mount the kids TV subtree (subPath kids) so fafflix can
              # resume playback of kids content started on cheeztv — same
              # underlying subvolume, different subtree, distinct mount path.
              mountPath: /media/tv-kids
              subPath: kids
              readOnly: true
            - name: media-movies
              # Adult instance: mount the movies/adult subtree of the shared
              # movies subvolume (subPath adult) as fafflix's primary movie
              # library.
              mountPath: /media/movies
              subPath: adult
              readOnly: true
            - name: media-movies
              # Also mount the kids movies subtree (subPath kids) for cross-resume
              # of kids content started on cheeztv.
              mountPath: /media/movies-kids
              subPath: kids
              readOnly: true
      volumes:
        - name: plugin-config
          configMap:
            name: fafflix-plugin-config
        - name: config
          persistentVolumeClaim:
            claimName: fafflix-config
        - name: transcode
          persistentVolumeClaim:
            claimName: fafflix-transcode
        - name: media-tv
          persistentVolumeClaim:
            claimName: fafflix-media-tv
        - name: media-movies
          persistentVolumeClaim:
            claimName: fafflix-media-movies
  volumeClaimTemplates:
    # Per-pod scratch cache — RWO, disposable, one PVC per replica.
    - metadata:
        name: cache
        annotations:
          # Exclude the per-pod cache PVCs from the fafflix-config k8up Schedule
          # (skipWithoutAnnotation is false cluster-wide). Cache is disposable and
          # RWO — it would also fail to mount into the backup pod while in use.
          k8up.io/backup: "false"
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 30Gi
        storageClassName: cephrbd-fast-delete
        volumeMode: Filesystem
