Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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