diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ec71cdd..f65e4d1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,38 +1,69 @@ -# TypeType 1.5.1 +# TypeType 1.6.0 -TypeType 1.5.1 fixes YouTube captions and improves anonymous YouTube session -startup. +TypeType 1.6.0 improves web playback, connected YouTube accounts, subscription feeds, downloads, authentication and notifications. ## Playback -- Load YouTube caption tracks directly from the viewer's browser instead of - routing timed-text requests through the instance egress. This avoids - subtitle-only HTTP 429 responses caused by shared server IP addresses. -- Preserve manual captions, automatically generated captions and translated - tracks. [#210](https://github.com/TypeType-Video/TypeType/issues/210) -- Keep subtitle handling for NicoNico and BiliBili unchanged. +- Resume playback after returning to a suspended browser tab instead of stopping or restarting from the beginning. [#219](https://github.com/TypeType-Video/TypeType/issues/219) +- Stop replaying the current video when autoplay is disabled. [#224](https://github.com/TypeType-Video/TypeType/issues/224) +- Show the correct resolution and codec labels in the quality selector. [#227](https://github.com/TypeType-Video/TypeType/issues/227) +- Preserve the exact playback position when changing video quality instead of jumping backward. [#229](https://github.com/TypeType-Video/TypeType/issues/229) +- Preserve saved progress through MSE source transitions and expire stale cached positions before resume. +- Keep Safari playback transitions bounded when autoplay permission or user activation has expired. -## YouTube Sessions +## YouTube Accounts -- Bind the BotGuard challenge and event identifier to the same page-native - YouTube context and visitor session. -- Use the current page context when generating anonymous YouTube tokens. +- Preserve the selected connected YouTube account through SABR preparation, token refresh and playback recovery. +- Bind the YouTube player and media tokens to the same selected account. +- Guide content requiring authentication to the YouTube account connection flow. +- Add an option to hide members-only videos. [#225](https://github.com/TypeType-Video/TypeType/issues/225) -## Self-Hosting +## Subscription Feeds -No configuration or database migration is required. +- Correctly classify scheduled, active and finished live streams when applying the live visibility setting. [#213](https://github.com/TypeType-Video/TypeType/issues/213) +- Preserve the original ordering of scheduled live streams instead of continually promoting them. +- Remove finished or stale live entries from subscription feeds. -Frontend, Server, Token and Downloader are aligned on version `1.5.1`. +## Subscription Groups API + +- Add the complete Server contract for named subscription groups. [#172](https://github.com/TypeType-Video/TypeType/issues/172) +- Create, rename and delete groups. +- Assign a subscribed channel to multiple groups. +- Filter subscriptions and feeds by group or show ungrouped channels. +- Preserve stable pagination while group membership changes. +- Include groups and memberships in TypeType backups. + +**Subscription groups are API-only in this release. There is no web interface for creating or managing groups yet.** The frontend integration remains tracked in [#172](https://github.com/TypeType-Video/TypeType/issues/172). + +## Accounts And Notifications + +- Fix initial OIDC installations requiring users to sign in twice. [#221](https://github.com/TypeType-Video/TypeType/issues/221) +- Add a setting to mute notification popups while keeping notifications available in the notification center. [#231](https://github.com/TypeType-Video/TypeType/issues/231) + +## Downloads + +- Allow downloads to work when Garage is only available through the internal TypeType network. A separate public Garage endpoint is no longer required. [#222](https://github.com/TypeType-Video/TypeType/issues/222) +- Keep artifact delivery behind the authenticated Server gateway. + +No configuration change or manual database migration is required. Server creates the subscription-group tables through its normal schema initialization. ## Thx -- A big thx to @Slashic for the detailed subtitle report and for suggesting the - client-side retrieval path. -- Thx as well to everyone testing beta and reporting playback issues. -- A big thx to [@Toastienergy](https://github.com/Toastienergy) and - [@filippobaroni](https://github.com/filippobaroni) for supporting TypeType - through GitHub Sponsors. Their support helps me cover the infrastructure and - spend more time improving the project. +A huge thx to @kapdon for implementing the complete subscription-groups Server contract and for the careful work on pagination, backups and tests. + +Thx to @CCGcastiel for proposing the live-stream visibility controls and helping improve subscription feeds. + +Thx to @arcoast for reporting the OIDC first-login problem and the Garage download configuration issue. + +Thx to @mfuchsberger for reporting the autoplay loop and proposing the option to hide members-only content. + +Thx to @therealresonix for the detailed quality selector and playback-position reports. + +Thx to @Toni-Vide for proposing the notification mute setting. + +A special thx to my sponsors @Toastienergy and @filippobaroni for supporting TypeType. + +Thx as well to everyone testing the beta, reporting playback problems, sharing logs, improving the documentation and helping other self-hosters. ## Updating diff --git a/TypeType-Downloader b/TypeType-Downloader index 68624aa..e27e618 160000 --- a/TypeType-Downloader +++ b/TypeType-Downloader @@ -1 +1 @@ -Subproject commit 68624aad523dca3b886efdceb98c934432fbdca7 +Subproject commit e27e61828ae5f80de736184f8eb39823b5acf9df diff --git a/TypeType-Frontend b/TypeType-Frontend index f6afc5e..d3c43fb 160000 --- a/TypeType-Frontend +++ b/TypeType-Frontend @@ -1 +1 @@ -Subproject commit f6afc5ebaed31151eca2f36ced008547d66ae3e9 +Subproject commit d3c43fba08f7116373bede6ad1f88091378022b2 diff --git a/TypeType-Player b/TypeType-Player index 1266d1a..6f07ef1 160000 --- a/TypeType-Player +++ b/TypeType-Player @@ -1 +1 @@ -Subproject commit 1266d1a776964f819a2eca7db00142a5f9ba1419 +Subproject commit 6f07ef18401f1df5a094c720007f7f5e81c25e7e diff --git a/TypeType-Server b/TypeType-Server index 8eae460..4717181 160000 --- a/TypeType-Server +++ b/TypeType-Server @@ -1 +1 @@ -Subproject commit 8eae460777883ce5989b14d632857917f6a2dcfd +Subproject commit 471718143ea6f42dffe626b340fe74dc4c6522e9 diff --git a/TypeType-Token b/TypeType-Token index 83371ef..ea097dd 160000 --- a/TypeType-Token +++ b/TypeType-Token @@ -1 +1 @@ -Subproject commit 83371ef0202e64559b1e62e23e3363bdd3a63658 +Subproject commit ea097dd36e43e2ea6a6b3ab84cba49807804f88a diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 25f78e5..8bed84d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -149,7 +149,7 @@ services: restart: unless-stopped garage-config: - image: busybox:1.37 + image: busybox:1.38 command: - /bin/sh - -ec diff --git a/docker-compose.yml b/docker-compose.yml index 1fe6da2..a942f3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -188,7 +188,7 @@ services: test: ["CMD", "redis-cli", "ping"] restart: unless-stopped garage-config: - image: busybox:1.37 + image: busybox:1.38 command: - /bin/sh - -ec diff --git a/scripts/bootstrap-env.sh b/scripts/bootstrap-env.sh index b2de019..c9fbcd0 100755 --- a/scripts/bootstrap-env.sh +++ b/scripts/bootstrap-env.sh @@ -16,11 +16,7 @@ generate_secret() { return fi - python3 - <<'PY' -import secrets - -print(secrets.token_urlsafe(48)) -PY + head -c 48 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n' } generate_hex_secret() { @@ -29,11 +25,7 @@ generate_hex_secret() { return fi - python3 - <<'PY' -import secrets - -print(secrets.token_hex(32)) -PY + od -An -N 32 -tx1 /dev/urandom | tr -d '[:space:]' } set_env_var() { diff --git a/scripts/bootstrap-garage.sh b/scripts/bootstrap-garage.sh index d6b02d4..047ef22 100755 --- a/scripts/bootstrap-garage.sh +++ b/scripts/bootstrap-garage.sh @@ -14,17 +14,7 @@ STATIC_RPC_SECRET="f4db2c1d5aef1dce278d4315b80425e98831714a48c059e22c2a39001b15c generate_hex() { local bytes="$1" - if command -v openssl >/dev/null 2>&1; then - openssl rand -hex "${bytes}" - return - fi - - python3 - "${bytes}" <<'PY' -import secrets -import sys - -print(secrets.token_hex(int(sys.argv[1]))) -PY + od -An -N "${bytes}" -tx1 /dev/urandom | tr -d '[:space:]' } generate_downloader_access_key() { diff --git a/scripts/install-stack.sh b/scripts/install-stack.sh index 3fa24e7..f7a30ee 100755 --- a/scripts/install-stack.sh +++ b/scripts/install-stack.sh @@ -141,17 +141,7 @@ confirm_tty() { generate_hex() { local bytes="$1" - if command -v openssl >/dev/null 2>&1; then - openssl rand -hex "${bytes}" - return - fi - - python3 - "${bytes}" <<'PY' -import secrets -import sys - -print(secrets.token_hex(int(sys.argv[1]))) -PY + od -An -N "${bytes}" -tx1 /dev/urandom | tr -d '[:space:]' } generate_downloader_access_key() { @@ -232,20 +222,24 @@ ensure_youtube_remote_login_env() { fi } -require_free_port() { +port_is_listening() { local port="$1" - python3 - < 1 && toupper(substr($2, length($2) - 3)) == port && $4 == "0A" { found = 1; exit } + END { exit found ? 0 : 1 } + ' "${socket_tables[@]}" + return + fi + (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null +} + +require_free_port() { + ! port_is_listening "$1" } get_env_var() { @@ -261,24 +255,17 @@ is_valid_port() { } find_random_free_port() { - python3 - <<'PY' -import random -import socket - -for _ in range(500): - port = random.randint(20000, 60999) - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.bind(("0.0.0.0", port)) - except OSError: - continue - finally: - s.close() - print(port) - break -else: - raise SystemExit("no free port found") -PY + local attempt + local port + for ((attempt = 0; attempt < 500; attempt++)); do + port=$((20000 + (((RANDOM << 15) | RANDOM) % 41000))) + if require_free_port "${port}"; then + echo "${port}" + return + fi + done + echo "[install] No free port found between 20000 and 60999." >&2 + return 1 } existing_stack_port() { diff --git a/scripts/install-stack.test.sh b/scripts/install-stack.test.sh index 9ce23fe..de8fe54 100755 --- a/scripts/install-stack.test.sh +++ b/scripts/install-stack.test.sh @@ -8,6 +8,7 @@ trap 'rm -rf "$temporary"' EXIT fake_bin="${temporary}/bin" install_dir="${temporary}/stack" docker_log="${temporary}/docker.log" +python_log="${temporary}/python.log" mkdir -p "$fake_bin" cat > "${fake_bin}/docker" <<'EOF' @@ -18,8 +19,16 @@ printf '\n' >> "$FAKE_DOCKER_LOG" EOF chmod +x "${fake_bin}/docker" +cat > "${fake_bin}/python3" <<'EOF' +#!/usr/bin/env bash +printf 'called\n' >> "$FAKE_PYTHON_LOG" +exit 99 +EOF +chmod +x "${fake_bin}/python3" + PATH="${fake_bin}:${PATH}" \ FAKE_DOCKER_LOG="$docker_log" \ +FAKE_PYTHON_LOG="$python_log" \ bash "${repository}/scripts/install-stack.sh" \ --source-dir "$repository" \ --dir "$install_dir" \ @@ -42,3 +51,11 @@ done grep -q '^compose version ' "$docker_log" grep -q 'compose .*config -q ' "$docker_log" grep -q '\[install\] Download-only complete\.' "${temporary}/install.log" +test ! -e "$python_log" + +downloader_access_key="$(grep '^DOWNLOADER_S3_ACCESS_KEY=' "${install_dir}/.env" | cut -d= -f2-)" +downloader_secret_key="$(grep '^DOWNLOADER_S3_SECRET_KEY=' "${install_dir}/.env" | cut -d= -f2-)" +remote_login_token="$(grep '^YOUTUBE_REMOTE_LOGIN_INTERNAL_TOKEN=' "${install_dir}/.env" | cut -d= -f2-)" +[[ "$downloader_access_key" =~ ^GK[0-9a-f]{24}$ ]] +[[ "$downloader_secret_key" =~ ^[0-9a-f]{64}$ ]] +[[ "$remote_login_token" =~ ^[A-Za-z0-9_-]{64}$ ]] diff --git a/scripts/setup-stack.sh b/scripts/setup-stack.sh index a380388..43cbefb 100755 --- a/scripts/setup-stack.sh +++ b/scripts/setup-stack.sh @@ -27,17 +27,7 @@ PLACEHOLDER_YOUTUBE_REMOTE_LOGIN_INTERNAL_TOKEN="SET_ME_SHARED_SECRET" generate_hex() { local bytes="$1" - if command -v openssl >/dev/null 2>&1; then - openssl rand -hex "${bytes}" - return - fi - - python3 - "${bytes}" <<'PY' -import secrets -import sys - -print(secrets.token_hex(int(sys.argv[1]))) -PY + od -An -N "${bytes}" -tx1 /dev/urandom | tr -d '[:space:]' } generate_downloader_access_key() { @@ -61,20 +51,24 @@ ensure_generated_secrets() { fi } -require_free_port() { +port_is_listening() { local port="$1" - python3 - < 1 && toupper(substr($2, length($2) - 3)) == port && $4 == "0A" { found = 1; exit } + END { exit found ? 0 : 1 } + ' "${socket_tables[@]}" + return + fi + (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null +} + +require_free_port() { + ! port_is_listening "$1" } set_env_var() { @@ -131,24 +125,17 @@ is_valid_port() { } find_random_free_port() { - python3 - <<'PY' -import random -import socket - -for _ in range(500): - port = random.randint(20000, 60999) - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.bind(("0.0.0.0", port)) - except OSError: - continue - finally: - s.close() - print(port) - break -else: - raise SystemExit("no free port found") -PY + local attempt + local port + for ((attempt = 0; attempt < 500; attempt++)); do + port=$((20000 + (((RANDOM << 15) | RANDOM) % 41000))) + if require_free_port "${port}"; then + echo "${port}" + return + fi + done + echo "[setup] No free port found between 20000 and 60999." >&2 + return 1 } is_arm64_host() {