Skip to content
77 changes: 54 additions & 23 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion TypeType-Frontend
Submodule TypeType-Frontend updated 60 files
+4 −0 .github/workflows/ci.yml
+2 −2 apps/web/package.json
+12 −0 apps/web/src/components/embed-error.tsx
+2 −2 apps/web/src/components/format-selector.tsx
+17 −4 apps/web/src/components/media-progress-events.tsx
+13 −4 apps/web/src/components/media-session-sync.tsx
+21 −4 apps/web/src/components/notification-toast-host.tsx
+31 −26 apps/web/src/components/sabr-mse-player.tsx
+23 −8 apps/web/src/components/shorts-error.tsx
+8 −0 apps/web/src/components/shorts-player-shell.tsx
+1 −0 apps/web/src/components/shorts-player-stage-types.ts
+7 −1 apps/web/src/components/shorts-player-stage.tsx
+1 −0 apps/web/src/components/video-player.tsx
+3 −3 apps/web/src/components/watch-stream-error.tsx
+15 −3 apps/web/src/hooks/use-blocked-filter.ts
+5 −1 apps/web/src/hooks/use-player-keyboard.ts
+4 −1 apps/web/src/hooks/use-progress.ts
+6 −1 apps/web/src/hooks/use-settings.ts
+3 −0 apps/web/src/hooks/use-shorts-active-stream.ts
+1 −1 apps/web/src/lib/api-auth-status.ts
+13 −9 apps/web/src/lib/api-collections.ts
+5 −2 apps/web/src/lib/api-youtube-session.ts
+5 −0 apps/web/src/lib/auth-routes.ts
+1 −0 apps/web/src/lib/blocked-content.ts
+11 −0 apps/web/src/lib/media-progress-position.ts
+8 −0 apps/web/src/lib/notification-toast-cursor.ts
+26 −0 apps/web/src/lib/progress-write-queue.ts
+115 −0 apps/web/src/lib/sabr-autoplay.ts
+3 −1 apps/web/src/lib/sabr-codec-capabilities.ts
+15 −12 apps/web/src/lib/sabr-quality-selection.ts
+6 −0 apps/web/src/lib/sabr-quality-tier.ts
+8 −6 apps/web/src/lib/sabr-source.ts
+22 −4 apps/web/src/lib/sabr-vidstack-bridge.ts
+31 −6 apps/web/src/lib/sabr-vidstack-provider.ts
+15 −0 apps/web/src/lib/video-visibility.ts
+8 −0 apps/web/src/lib/watch-resume.ts
+8 −1 apps/web/src/lib/youtube-session-route.ts
+7 −2 apps/web/src/routes/__root.tsx
+4 −3 apps/web/src/routes/embed_.$videoId.tsx
+7 −3 apps/web/src/routes/watch.tsx
+1 −0 apps/web/src/settings/hide-everything-toggle.tsx
+8 −0 apps/web/src/settings/settings-content-toggles.tsx
+20 −0 apps/web/src/settings/settings-landing-page.tsx
+2 −0 apps/web/src/types/user.ts
+31 −0 apps/web/tests/api-auth-status.test.ts
+14 −0 apps/web/tests/auth-routes.test.ts
+31 −0 apps/web/tests/media-progress-position.test.ts
+8 −0 apps/web/tests/notification-toast-cursor.test.ts
+55 −0 apps/web/tests/progress-write-queue.test.ts
+200 −0 apps/web/tests/sabr-autoplay.test.ts
+10 −0 apps/web/tests/sabr-default-quality.test.ts
+32 −0 apps/web/tests/sabr-quality-label.test.ts
+18 −2 apps/web/tests/sabr-quality-selection.test.ts
+89 −0 apps/web/tests/sabr-vidstack-bridge.test.ts
+61 −0 apps/web/tests/sabr-vidstack-provider.test.ts
+20 −0 apps/web/tests/video-visibility.test.ts
+7 −1 apps/web/tests/watch-resume.test.ts
+51 −0 apps/web/tests/youtube-session-errors.test.ts
+3 −3 bun.lock
+1 −1 package.json
2 changes: 1 addition & 1 deletion TypeType-Server
Submodule TypeType-Server updated 133 files
2 changes: 1 addition & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ services:
restart: unless-stopped

garage-config:
image: busybox:1.37
image: busybox:1.38
command:
- /bin/sh
- -ec
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions scripts/bootstrap-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
12 changes: 1 addition & 11 deletions scripts/bootstrap-garage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
71 changes: 29 additions & 42 deletions scripts/install-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -232,20 +222,24 @@ ensure_youtube_remote_login_env() {
fi
}

require_free_port() {
port_is_listening() {
local port="$1"
python3 - <<PY
import socket, sys
port = int(${port})
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(("0.0.0.0", port))
except OSError:
sys.exit(1)
finally:
s.close()
sys.exit(0)
PY
local port_hex
local socket_tables=(/proc/net/tcp)
if [[ -r /proc/net/tcp ]]; then
[[ ! -r /proc/net/tcp6 ]] || socket_tables+=(/proc/net/tcp6)
port_hex="$(printf '%04X' "${port}")"
awk -v port="${port_hex}" '
NR > 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() {
Expand All @@ -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() {
Expand Down
17 changes: 17 additions & 0 deletions scripts/install-stack.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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" \
Expand All @@ -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}$ ]]
71 changes: 29 additions & 42 deletions scripts/setup-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -61,20 +51,24 @@ ensure_generated_secrets() {
fi
}

require_free_port() {
port_is_listening() {
local port="$1"
python3 - <<PY
import socket, sys
port = int(${port})
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(("0.0.0.0", port))
except OSError:
sys.exit(1)
finally:
s.close()
sys.exit(0)
PY
local port_hex
local socket_tables=(/proc/net/tcp)
if [[ -r /proc/net/tcp ]]; then
[[ ! -r /proc/net/tcp6 ]] || socket_tables+=(/proc/net/tcp6)
port_hex="$(printf '%04X' "${port}")"
awk -v port="${port_hex}" '
NR > 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() {
Expand Down Expand Up @@ -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() {
Expand Down