Author SHA1 Message Date
Bram b20ebbfca6 plex excluder
Build and Push Docker Images / build-and-push (push) Successful in 1m14s
2026-09-05 23:25:26 +02:00
Bram 07924fe892 dns proxy
Build and Push Docker Images / build-and-push (push) Failing after 52s
2026-08-30 23:00:48 +02:00
gitea-actions 23a2d1964a chore(jellyfin): update plugin catalog [skip ci] 2026-08-22 20:31:14 +00:00
Bram 54712b7dae really?
Build and Release Jellyfin plugins / release (push) Successful in 34s
2026-08-22 22:30:38 +02:00
Bram 727b4a0970 ugh
Build and Release Jellyfin plugins / release (push) Successful in 21s
2026-08-22 22:29:00 +02:00
Bram 1aa7e83044 Update Jellyfin plugin release workflow to use MD5 checksums instead of SHA256. Modify manifest.json with new source URL and checksum, and update README to clarify checksum verification process.
Build and Release Jellyfin plugins / release (push) Successful in 23s
2026-08-22 22:25:47 +02:00
gitea-actions 90c4423696 chore(jellyfin): update plugin catalog [skip ci] 2026-08-22 20:19:39 +00:00
17 changed files with 726 additions and 4 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ jobs:
exit 1 exit 1
fi fi
ARTIFACT_NAME=$(basename "$ARTIFACT") ARTIFACT_NAME=$(basename "$ARTIFACT")
CHECKSUM=$(sha256sum "$ARTIFACT" | awk '{print $1}') CHECKSUM=$(md5sum "$ARTIFACT" | awk '{print $1}')
BODY=$(printf 'Jellyfin plugin **%s** %s\n\nBuilt automatically from Jellyfin/%s changes.' "$SLUG" "$VERSION" "$SLUG") BODY=$(printf 'Jellyfin plugin **%s** %s\n\nBuilt automatically from Jellyfin/%s changes.' "$SLUG" "$VERSION" "$SLUG")
BODY_JSON=$(printf '%s' "$BODY" | jq -Rs .) BODY_JSON=$(printf '%s' "$BODY" | jq -Rs .)
+13
View File
@@ -0,0 +1,13 @@
FROM caddy:2-alpine
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENV LISTEN_PORT=8080 \
TARGET_URL="" \
PROXY_INSECURE_TLS=0 \
PRESERVE_HOST=0
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
@@ -0,0 +1,15 @@
services:
dns-proxy:
build: .
ports:
- "8080:8080"
environment:
TARGET_URL: "https://internal.example.com"
# PROXY_INSECURE_TLS: "1" # if upstream uses a self-signed cert
# PRESERVE_HOST: "1" # keep the client's Host header instead of the upstream host
dns:
- 10.0.0.53 # custom DNS required to resolve TARGET_URL
# Extra hosts are also an option instead of / alongside custom DNS:
# extra_hosts:
# - "internal.example.com:10.0.0.10"
restart: unless-stopped
+59
View File
@@ -0,0 +1,59 @@
#!/bin/sh
set -eu
TARGET_URL="${TARGET_URL:-}"
LISTEN_PORT="${LISTEN_PORT:-8080}"
PROXY_INSECURE_TLS="${PROXY_INSECURE_TLS:-0}"
PRESERVE_HOST="${PRESERVE_HOST:-0}"
if [ -z "$TARGET_URL" ]; then
echo "TARGET_URL is required (e.g. https://internal.example.com)" >&2
exit 1
fi
case "$TARGET_URL" in
http://*|https://*) ;;
*)
echo "TARGET_URL must start with http:// or https://" >&2
exit 1
;;
esac
# Strip trailing slash so paths concatenate cleanly
TARGET_URL="${TARGET_URL%/}"
CADDYFILE="/etc/caddy/Caddyfile"
mkdir -p /etc/caddy
EXTRA_DIRECTIVES=""
if [ "$PRESERVE_HOST" = "1" ]; then
EXTRA_DIRECTIVES="${EXTRA_DIRECTIVES}
header_up Host {http.request.host}"
fi
if [ "$PROXY_INSECURE_TLS" = "1" ]; then
EXTRA_DIRECTIVES="${EXTRA_DIRECTIVES}
transport http {
tls_insecure_skip_verify
}"
fi
cat > "$CADDYFILE" <<EOF
{
admin off
auto_https off
}
:${LISTEN_PORT} {
reverse_proxy ${TARGET_URL} {${EXTRA_DIRECTIVES}
}
}
EOF
echo "Proxying :${LISTEN_PORT} -> ${TARGET_URL}"
if [ "$PROXY_INSECURE_TLS" = "1" ]; then
echo "TLS verification disabled for upstream"
fi
exec caddy run --config "$CADDYFILE" --adapter caddyfile
+10
View File
@@ -0,0 +1,10 @@
__pycache__/
*.pyc
*.pyo
.env
.venv/
venv/
.git/
.idea/
.vscode/
*.md
+22
View File
@@ -0,0 +1,22 @@
PLEX_URL=http://10.2.0.100:32400
PLEX_TOKEN=
JELLYSEERR_URL=http://10.2.0.102:4022
JELLYSEERR_API_KEY=
# Formaat: Label:user1,user2;AnderLabel:user3
# Die users mogen het label zien; alle andere Plex-users krijgen label! exclusion.
LABEL_ACCESS=Debby:debby961
# Cron (5-veld) of macro: @hourly @daily @weekly …
CRON_SCHEDULE=0 */6 * * *
RUN_ON_START=true
DRY_RUN=false
TZ=Europe/Amsterdam
# Alleen nodig als Plex andere paden gebruikt dan de mounts hieronder.
# Defaults: /mnt/Series en /mnt/Movies (mount je volumes daarheen).
# PLEX_SERIES_PREFIX=/mnt/Series
# PLEX_MOVIES_PREFIX=/mnt/Movies
# LOCAL_SERIES_ROOT=/mnt/Series
# LOCAL_MOVIES_ROOT=/mnt/Movies
+5
View File
@@ -0,0 +1,5 @@
.env
__pycache__/
*.pyc
.venv/
venv/
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ=Europe/Amsterdam \
PLEX_SERIES_PREFIX=/mnt/Series \
PLEX_MOVIES_PREFIX=/mnt/Movies \
CRON_SCHEDULE="0 */6 * * *" \
RUN_ON_START=true \
DRY_RUN=false
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends tzdata ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY sync.py entrypoint.py ./
ENTRYPOINT ["python", "entrypoint.py"]
@@ -0,0 +1,18 @@
services:
plex-label-sync:
build: .
container_name: plex-label-sync
env_file:
- .env
environment:
TZ: Europe/Amsterdam
# Label:users — meerdere labels met ; gescheiden
# LABEL_ACCESS: "Debby:debby961;Kids:alice,bob"
CRON_SCHEDULE: "0 */6 * * *"
RUN_ON_START: "true"
DRY_RUN: "false"
volumes:
# Zelfde paden als in Plex → geen LOCAL_*/PLEX_* prefix-env nodig
- /path/to/Series:/mnt/Series
- /path/to/Movies:/mnt/Movies
restart: unless-stopped
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Run plex-label-sync on a cron schedule from env."""
from __future__ import annotations
import os
import subprocess
import sys
import time
from datetime import datetime
from zoneinfo import ZoneInfo
from croniter import croniter
CRON_MACROS = {
'@yearly': '0 0 1 1 *',
'@annually': '0 0 1 1 *',
'@monthly': '0 0 1 * *',
'@weekly': '0 0 * * 0',
'@daily': '0 0 * * *',
'@midnight': '0 0 * * *',
'@hourly': '0 * * * *',
}
def log(msg: str) -> None:
print(f"[{datetime.now().astimezone().isoformat(timespec='seconds')}] {msg}", flush=True)
def zone() -> ZoneInfo:
name = os.environ.get('TZ') or 'UTC'
try:
return ZoneInfo(name)
except Exception as exc:
raise SystemExit(f"Invalid TZ '{name}': {exc}") from exc
def resolve_cron_expr() -> str:
for key in ('CRON_SCHEDULE', 'CRON', 'SYNC_CRON'):
value = os.environ.get(key, '').strip()
if value:
expr = CRON_MACROS.get(value.lower(), value)
if not croniter.is_valid(expr):
raise SystemExit(
f"Invalid {key} '{value}' (expected a 5-field cron expression)"
)
return expr
return '0 */6 * * *'
def next_run(expr: str, after: datetime) -> datetime:
return croniter(expr, after).get_next(datetime)
def run_sync() -> int:
log('Starting sync…')
result = subprocess.run([sys.executable, '/app/sync.py'], check=False)
log(f'Sync finished with code {result.returncode}.')
return result.returncode
def main() -> int:
tz = zone()
expr = resolve_cron_expr()
run_on_start = os.environ.get('RUN_ON_START', 'true').lower() in {'1', 'true', 'yes'}
log(f"plex-label-sync scheduler (TZ={tz.key}, CRON_SCHEDULE='{expr}')")
if run_on_start:
run_sync()
while True:
now = datetime.now(tz=tz)
nxt = next_run(expr, now)
sleep_for = max(1.0, (nxt - now).total_seconds())
log(f"Next run at {nxt.isoformat(timespec='seconds')} (sleep {sleep_for:.0f}s)")
time.sleep(sleep_for)
run_sync()
if __name__ == '__main__':
sys.exit(main())
+4
View File
@@ -0,0 +1,4 @@
plexapi
requests
python-dotenv
croniter
+452
View File
@@ -0,0 +1,452 @@
#!/usr/bin/env python3
"""Label Jellyseerr requests in Plex and exclude those labels from other users."""
from __future__ import annotations
from pathlib import Path
import os
import sys
import requests
from dotenv import load_dotenv
from plexapi.server import PlexServer
load_dotenv()
PLEX_URL = os.environ['PLEX_URL']
PLEX_TOKEN = os.environ['PLEX_TOKEN']
JELLYSEERR_URL = os.environ['JELLYSEERR_URL'].rstrip('/')
JELLYSEERR_API_KEY = os.environ['JELLYSEERR_API_KEY']
PLEX_SERIES_PREFIX = os.environ.get('PLEX_SERIES_PREFIX', '/mnt/Series')
LOCAL_SERIES_ROOT = Path(os.environ.get('LOCAL_SERIES_ROOT', PLEX_SERIES_PREFIX))
PLEX_MOVIES_PREFIX = os.environ.get('PLEX_MOVIES_PREFIX', '/mnt/Movies')
LOCAL_MOVIES_ROOT = Path(os.environ.get('LOCAL_MOVIES_ROOT', PLEX_MOVIES_PREFIX))
IGNORE_FILENAME = '.ignore'
DRY_RUN = os.environ.get('DRY_RUN', 'false').lower() in {'1', 'true', 'yes'}
def parse_label_access(raw: str) -> dict[str, list[str]]:
"""Parse LABEL_ACCESS=Debby:debby961;Kids:alice,bob into {label: [users]}."""
mapping: dict[str, list[str]] = {}
for entry in raw.split(';'):
entry = entry.strip()
if not entry:
continue
if ':' not in entry:
raise SystemExit(
f"Ongeldige LABEL_ACCESS entry '{entry}' "
"(verwacht Label:user1,user2;AnderLabel:user3)"
)
label, users_raw = entry.split(':', 1)
label = label.strip()
users = [name.strip() for name in users_raw.split(',') if name.strip()]
if not label or not users:
raise SystemExit(
f"Ongeldige LABEL_ACCESS entry '{entry}' "
"(label en minstens één gebruiker vereist)"
)
mapping[label] = users
if not mapping:
raise SystemExit(
'LABEL_ACCESS is leeg. Voorbeeld: Debby:debby961;Kids:alice,bob'
)
return mapping
def load_label_access() -> dict[str, list[str]]:
raw = os.environ.get('LABEL_ACCESS', '').strip()
if raw:
return parse_label_access(raw)
# Backward-compatible single-label env vars
label = os.environ.get('LABEL', '').strip()
plex_user = os.environ.get('PLEX_USER', '').strip()
skip_users = [
name.strip()
for name in os.environ.get('SKIP_USERS', plex_user).split(',')
if name.strip()
]
if label and skip_users:
return {label: skip_users}
raise SystemExit(
'Stel LABEL_ACCESS in (bijv. Debby:debby961) '
'of LABEL + SKIP_USERS / PLEX_USER.'
)
LABEL_ACCESS = load_label_access()
jellyseerr = requests.Session()
jellyseerr.headers['X-Api-Key'] = JELLYSEERR_API_KEY
jellyseerr.headers['Accept'] = 'application/json'
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
account = plex.myPlexAccount()
def jellyseerr_get(path, params=None):
response = jellyseerr.get(f'{JELLYSEERR_URL}/api/v1{path}', params=params, timeout=30)
response.raise_for_status()
return response.json()
def find_jellyseerr_user(plex_username):
data = jellyseerr_get('/user', params={'q': plex_username, 'take': 50})
needle = plex_username.lower()
for user in data.get('results', []):
fields = [
user.get('plexUsername'),
user.get('username'),
user.get('displayName'),
user.get('email'),
]
if any(field and field.lower() == needle for field in fields):
return user
raise SystemExit(f'Jellyseerr-gebruiker niet gevonden: {plex_username}')
def fetch_user_requests(user_id):
skip = 0
take = 50
requests_found = []
while True:
data = jellyseerr_get(f'/user/{user_id}/requests', params={'take': take, 'skip': skip})
page = data.get('results', [])
requests_found.extend(page)
total = data.get('pageInfo', {}).get('results', 0)
skip += take
if skip >= total or not page:
break
return requests_found
def media_title(request_type, tmdb_id, fallback):
endpoint = f'/tv/{tmdb_id}' if request_type == 'tv' else f'/movie/{tmdb_id}'
title_key = 'name' if request_type == 'tv' else 'title'
try:
details = jellyseerr_get(endpoint)
return details.get(title_key) or fallback
except Exception:
return fallback
def find_plex_item(media, request_type):
libtype = 'show' if request_type == 'tv' else 'movie'
rating_key = media.get('ratingKey') or media.get('ratingKey4k')
if rating_key:
try:
item = plex.fetchItem(int(rating_key))
item_type = getattr(item, 'type', None)
if request_type == 'tv':
if item_type == 'show':
return item
if item_type in {'season', 'episode'} and getattr(item, 'show', None):
return item.show()
elif item_type == 'movie':
return item
except Exception:
pass
guids = []
tmdb_id = media.get('tmdbId')
if tmdb_id:
guids.append(f'tmdb://{tmdb_id}')
if request_type == 'tv':
tvdb_id = media.get('tvdbId')
if tvdb_id:
guids.append(f'tvdb://{tvdb_id}')
for guid in guids:
matches = plex.library.search(libtype=libtype, guid=guid)
if matches:
return matches[0]
return None
def has_label(item, label):
return any(tag.tag == label for tag in getattr(item, 'labels', []))
def local_media_dirs(item, plex_prefix, local_root):
"""Map Plex library paths to local folders."""
dirs = []
for location in getattr(item, 'locations', None) or []:
path = Path(location)
try:
relative = path.relative_to(plex_prefix)
except ValueError:
if path.is_absolute() and local_root in path.parents:
dirs.append(path if path.is_dir() else path.parent)
continue
local = local_root / relative
dirs.append(local if local.is_dir() or not local.suffix else local.parent)
return dirs
def ensure_ignore_file(media_dir):
ignore_path = media_dir / IGNORE_FILENAME
if not media_dir.is_dir():
return 'missing_dir', ignore_path
if ignore_path.exists():
return 'exists', ignore_path
if DRY_RUN:
return 'would_create', ignore_path
ignore_path.touch()
return 'created', ignore_path
def parse_filters(raw):
"""Zet Plex filterstring om naar dict, bijv. {'label!': ['Kids'], 'contentRating': ['G']}."""
filters = {}
if not raw:
return filters
decoded = raw.replace('%2C', ',').replace('%20', ' ')
for part in decoded.replace('|', '&').split('&'):
if '=' not in part:
continue
key, value = part.split('=', 1)
labels = [item.strip() for item in value.split(',') if item.strip()]
if labels:
filters[key] = labels
return filters
def build_filter_string(filters):
parts = []
for key, values in filters.items():
if values:
parts.append(f"{key}={','.join(values)}")
return '&'.join(parts)
def user_matches(user, names):
fields = [user.title, getattr(user, 'username', None), getattr(user, 'email', None)]
lowered = {name.lower() for name in names}
return any(field and field.lower() in lowered for field in fields)
def display_name(user):
extras = [value for value in (user.username, user.email) if value and value != user.title]
if extras:
return f"{user.title} ({', '.join(extras)})"
return user.title
def apply_label_exclusions(filters, allowed_labels: set[str], excluded_labels: set[str]):
"""Zorg dat allowed labels zichtbaar blijven en excluded labels in label! staan."""
current = list(filters.get('label!', []))
changed = False
for label in allowed_labels:
if label in current:
current = [item for item in current if item != label]
changed = True
for label in excluded_labels:
if label not in current:
current.append(label)
changed = True
filters = dict(filters)
if current:
filters['label!'] = current
elif 'label!' in filters:
del filters['label!']
changed = True
return filters, changed
def update_user_filters(user_id, filter_television=None, filter_movies=None):
params = {'X-Plex-Token': PLEX_TOKEN}
if filter_television is not None:
params['filterTelevision'] = filter_television
if filter_movies is not None:
params['filterMovies'] = filter_movies
response = requests.put(
f'https://plex.tv/api/users/{user_id}',
params=params,
timeout=30,
)
response.raise_for_status()
return response
def process_requests(requests_list, request_type, label, plex_prefix, local_root):
kind = 'TV' if request_type == 'tv' else 'Film'
print(f"\n=== [{label}] {kind}-requests: {len(requests_list)} ===")
labeled = 0
already = 0
missing = 0
failed = 0
ignore_created = 0
ignore_exists = 0
ignore_missing_dir = 0
ignore_failed = 0
seen_keys = set()
for request in requests_list:
media = request.get('media') or {}
tmdb_id = media.get('tmdbId')
dedupe_key = media.get('ratingKey') or media.get('ratingKey4k') or tmdb_id
if dedupe_key in seen_keys:
continue
seen_keys.add(dedupe_key)
title = media_title(request_type, tmdb_id, media.get('externalServiceSlug') or f'tmdb:{tmdb_id}')
item = find_plex_item(media, request_type)
if not item:
print(f"Niet in Plex: {title}")
missing += 1
continue
if has_label(item, label):
print(f"Al gelabeld: {item.title}")
already += 1
else:
print(f"Label toevoegen ({label}): {item.title}")
if not DRY_RUN:
try:
item.addLabel(label)
labeled += 1
except Exception as error:
print(f" Fout label: {error}")
failed += 1
else:
labeled += 1
media_dirs = local_media_dirs(item, plex_prefix, local_root)
if not media_dirs:
print(f" Geen lokale map gevonden voor {item.title}")
ignore_missing_dir += 1
continue
for media_dir in media_dirs:
try:
status, ignore_path = ensure_ignore_file(media_dir)
except Exception as error:
print(f" Fout .ignore in {media_dir}: {error}")
ignore_failed += 1
continue
if status == 'created':
print(f" .ignore gezet: {ignore_path}")
ignore_created += 1
elif status == 'would_create':
print(f" zou .ignore zetten: {ignore_path}")
ignore_created += 1
elif status == 'exists':
print(f" .ignore bestond al: {ignore_path}")
ignore_exists += 1
elif status == 'missing_dir':
print(f" map ontbreekt lokaal: {media_dir}")
ignore_missing_dir += 1
action = 'zou labelen' if DRY_RUN else 'gelabeld'
ignore_action = 'zou zetten' if DRY_RUN else 'gezet'
print(
f"\n[{label}] {kind} klaar: {labeled} {action}, {already} hadden het label al, "
f"{missing} niet in Plex, {failed} label-fouten."
)
print(
f".ignore: {ignore_created} {ignore_action}, {ignore_exists} bestonden al, "
f"{ignore_missing_dir} map ontbreekt, {ignore_failed} fouten."
)
def sync_label_from_jellyseerr(label: str, plex_usernames: list[str]):
seen_user_ids = set()
for plex_username in plex_usernames:
user = find_jellyseerr_user(plex_username)
user_id = user['id']
if user_id in seen_user_ids:
continue
seen_user_ids.add(user_id)
print(
f"\nJellyseerr-gebruiker voor label '{label}': "
f"{user.get('displayName')} (id {user_id}, {user.get('requestCount')} requests) "
f"[match: {plex_username}]"
)
all_requests = fetch_user_requests(user_id)
tv_requests = [item for item in all_requests if item.get('type') == 'tv']
movie_requests = [item for item in all_requests if item.get('type') == 'movie']
process_requests(tv_requests, 'tv', label, PLEX_SERIES_PREFIX, LOCAL_SERIES_ROOT)
process_requests(movie_requests, 'movie', label, PLEX_MOVIES_PREFIX, LOCAL_MOVIES_ROOT)
def sync_user_filters():
print('\n=== Plex-filters (TV + Films) ===')
all_labels = set(LABEL_ACCESS)
updated = 0
skipped = 0
failed = 0
for user in account.users():
name = display_name(user)
allowed = {label for label, users in LABEL_ACCESS.items() if user_matches(user, users)}
excluded = all_labels - allowed
tv_filters, tv_changed = apply_label_exclusions(
parse_filters(user.filterTelevision), allowed, excluded
)
movie_filters, movie_changed = apply_label_exclusions(
parse_filters(user.filterMovies), allowed, excluded
)
if not tv_changed and not movie_changed:
print(f"Al ingesteld: {name} (zichtbaar: {sorted(allowed) or '-'})")
skipped += 1
continue
tv_string = build_filter_string(tv_filters)
movie_string = build_filter_string(movie_filters)
print(f"Aanpassen: {name}")
print(f" mag zien: {sorted(allowed) or '-'}")
print(f" exclude: {sorted(excluded) or '-'}")
if tv_changed:
print(f" TV-filters: {tv_string or '(leeg)'}")
if movie_changed:
print(f" Film-filters: {movie_string or '(leeg)'}")
if DRY_RUN:
updated += 1
continue
try:
update_user_filters(
user.id,
filter_television=tv_string if tv_changed else None,
filter_movies=movie_string if movie_changed else None,
)
updated += 1
except Exception as error:
print(f" Fout: {error}")
failed += 1
action = 'zou aanpassen' if DRY_RUN else 'aangepast'
print(f"\nFilters: {updated} gebruiker(s) {action}, {skipped} overgeslagen, {failed} mislukt.")
def main():
if DRY_RUN:
print('DRY_RUN actief — er wordt niets weggeschreven.\n')
print('Label-toegang:')
for label, users in LABEL_ACCESS.items():
print(f" {label}: {', '.join(users)}")
for label, users in LABEL_ACCESS.items():
sync_label_from_jellyseerr(label, users)
sync_user_filters()
return 0
if __name__ == '__main__':
sys.exit(main())
+1
View File
@@ -0,0 +1 @@
latest
+2
View File
@@ -31,4 +31,6 @@ Bump `Jellyfin/<slug>/version` before pushing when you want a new release. Re-pu
|---|---|---| |---|---|---|
| `PUBLIC_GITEA_URL` | `https://gitea.example.com` | `sourceUrl` in `manifest.json` (no trailing slash) | | `PUBLIC_GITEA_URL` | `https://gitea.example.com` | `sourceUrl` in `manifest.json` (no trailing slash) |
Jellyfin verifies package **MD5** checksums (not SHA256). The release workflow writes `md5sum` into the catalog.
API calls still use the runners internal Gitea URL; only catalog download links use this secret. API calls still use the runners internal Gitea URL; only catalog download links use this secret.
@@ -1,6 +1,6 @@
name: "Personal Recordings" name: "Personal Recordings"
guid: "7f3e9c2a-4b1d-4e8f-9a6c-2d5e8f1b3c4a" guid: "7f3e9c2a-4b1d-4e8f-9a6c-2d5e8f1b3c4a"
version: "1.0.0.0" version: "1.0.1.0"
targetAbi: "10.11.0.0" targetAbi: "10.11.0.0"
framework: "net9.0" framework: "net9.0"
owner: "bram" owner: "bram"
@@ -1 +1 @@
1.0.0 1.0.1
+17 -1
View File
@@ -7,11 +7,27 @@
"owner": "bram", "owner": "bram",
"category": "Live TV", "category": "Live TV",
"versions": [ "versions": [
{
"version": "1.0.1.0",
"changelog": "Release 1.0.1",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://gitea.bramkelchtermans.be/Bram/projects/releases/download/jellyfin-plugin-personal-recordings-v1.0.1/personal-recordings_1.0.1.0.zip",
"checksum": "69bb2094dc7eb81e674e0afac42cedfd",
"timestamp": "2026-08-22T20:31:14Z"
},
{ {
"version": "1.0.0.0", "version": "1.0.0.0",
"changelog": "Release 1.0.0", "changelog": "Release 1.0.0",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "http://10.2.0.104:3000/Bram/projects/releases/download/jellyfin/jellyfin-plugin-personal-recordings/v1.0.0/personal-recordings_1.0.0.0.zip", "sourceUrl": "https://gitea.bramkelchtermans.be/Bram/projects/releases/download/jellyfin-plugin-personal-recordings-v1.0.0/personal-recordings_1.0.0.0.zip",
"checksum": "63ed4d39770330b8fd714f116cd57f4c1e8744b88afb9865ccbed6599f043b49",
"timestamp": "2026-08-22T20:19:39Z"
},
{
"version": "1.0.0.0",
"changelog": "Release 1.0.0",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://gitea.bramkelchtermans.be/Bram/projects/releases/download/jellyfin-plugin-personal-recordings-v1.0.0/personal-recordings_1.0.0.0.zip",
"checksum": "66cd0155334fd10b77d35af8ddba07d0e2e60fa886bd63369a5e0d4ce289a441", "checksum": "66cd0155334fd10b77d35af8ddba07d0e2e60fa886bd63369a5e0d4ce289a441",
"timestamp": "2026-08-22T20:07:03Z" "timestamp": "2026-08-22T20:07:03Z"
} }