From 7d9490faa9b18e6d9cff1e86270ff35e121dfb76 Mon Sep 17 00:00:00 2001 From: D3SOX Date: Fri, 14 Aug 2026 09:03:14 +0200 Subject: [PATCH 1/4] feat(sync): deprecate playback speed API --- README.md | 15 +++++++++++- src/handlers/channel_playback_speeds.rs | 31 ++++++++++++++++++++++--- src/handlers/encrypted_sync.rs | 4 ++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 23c7fd5..a7c3da3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OpenTubeX Sync Server -Server to synchronize OpenTubeX data between devices, including subscriptions, playlists, watch history, and channel playback speeds. +Server to synchronize OpenTubeX data between devices, including subscriptions, playlists, watch history, profiles, sessions, and settings. OpenTubeX clients can use the encrypted sync API exposed by this server. In that mode all synchronized content is encrypted on the client with a separate @@ -14,6 +14,19 @@ that may already exist. After that first upload, plaintext sync endpoints are rejected for the account so an older client cannot accidentally repopulate readable data. +### Deprecated playback-speed API + +The dedicated `/v1/channel_playback_speeds` endpoints and encrypted +`playbackSpeeds` collection are deprecated. Current OpenTubeX clients store all +saved channel preferences, including playback speeds, in the encrypted +`settings` collection. + +Both deprecated forms remain fully functional during the client migration +period. Their database table, encrypted collection support, legacy migration, +and cleanup logic must only be removed after supported clients no longer use +them. Responses from the dedicated plaintext endpoints include the standard +`Deprecation` header; no removal date has been scheduled. + This project is based on the [LibreTube sync server](https://github.com/libre-tube/sync-server). ## Running diff --git a/src/handlers/channel_playback_speeds.rs b/src/handlers/channel_playback_speeds.rs index 7348b00..16b9f4f 100644 --- a/src/handlers/channel_playback_speeds.rs +++ b/src/handlers/channel_playback_speeds.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use actix_web::{HttpResponse, Responder, delete, get, middleware::from_fn, put, web}; use diesel_async::{AsyncConnection, scoped_futures::ScopedFutureExt}; use utoipa_actix_web::scope; @@ -18,6 +20,8 @@ use crate::{ models::{Account, ChannelPlaybackSpeed}, }; +const DEPRECATION_HEADER: (&str, &str) = ("Deprecation", "@1786665600"); + pub struct ChannelPlaybackSpeedsHandler; impl ScopedHandler for ChannelPlaybackSpeedsHandler { @@ -38,6 +42,11 @@ impl ScopedHandler for ChannelPlaybackSpeedsHandler { } } +/// Get saved channel playback speeds through the deprecated dedicated API. +/// +/// New OpenTubeX clients sync saved channel preferences through the encrypted +/// `settings` collection. This endpoint remains available for older clients. +#[deprecated(note = "use the encrypted settings collection for saved channel preferences")] #[utoipa::path(responses((status = OK, body = Vec)), security(("api_jwt_token" = [])))] #[get("/")] async fn get_channel_playback_speeds( @@ -49,9 +58,16 @@ async fn get_channel_playback_speeds( .await .map_err(|_| HandlerError::InternalDatabaseError)?; - Ok(HttpResponse::Ok().json(speeds)) + Ok(HttpResponse::Ok() + .insert_header(DEPRECATION_HEADER) + .json(speeds)) } +/// Save a channel playback speed through the deprecated dedicated API. +/// +/// New OpenTubeX clients sync saved channel preferences through the encrypted +/// `settings` collection. This endpoint remains available for older clients. +#[deprecated(note = "use the encrypted settings collection for saved channel preferences")] #[utoipa::path(responses((status = OK, body = ChannelPlaybackSpeed)), security(("api_jwt_token" = [])))] #[put("/")] async fn put_channel_playback_speed( @@ -71,7 +87,9 @@ async fn put_channel_playback_speed( let mut conn = get_db_conn!(pool); store_playback_speed(&mut conn, &speed).await?; - Ok(HttpResponse::Ok().json(speed)) + Ok(HttpResponse::Ok() + .insert_header(DEPRECATION_HEADER) + .json(speed)) } /// Store a playback speed, enforcing the row quota in the same transaction. @@ -103,6 +121,11 @@ async fn store_playback_speed( .await } +/// Delete a channel playback speed through the deprecated dedicated API. +/// +/// New OpenTubeX clients sync saved channel preferences through the encrypted +/// `settings` collection. This endpoint remains available for older clients. +#[deprecated(note = "use the encrypted settings collection for saved channel preferences")] #[utoipa::path(responses((status = OK)), security(("api_jwt_token" = [])))] #[delete("/{channel_id}")] async fn delete_channel_playback_speed( @@ -115,5 +138,7 @@ async fn delete_channel_playback_speed( .await .map_err(|_| HandlerError::InternalDatabaseError)?; - Ok(HttpResponse::Ok()) + Ok(HttpResponse::Ok() + .insert_header(DEPRECATION_HEADER) + .finish()) } diff --git a/src/handlers/encrypted_sync.rs b/src/handlers/encrypted_sync.rs index 89b4e5d..cc38360 100644 --- a/src/handlers/encrypted_sync.rs +++ b/src/handlers/encrypted_sync.rs @@ -17,6 +17,8 @@ use crate::{WebData, get_db_conn}; const MEBIBYTE: usize = 1024 * 1024; const MAX_ENCRYPTED_SYNC_BYTES: usize = 64 * MEBIBYTE; const MAX_ENCRYPTED_SYNC_ACCOUNT_BYTES: usize = 128 * MEBIBYTE; +// `playbackSpeeds` is deprecated for new clients, but remains part of legacy +// document migration until older OpenTubeX versions have been phased out. const LEGACY_ENCRYPTED_COLLECTIONS: [&str; 6] = [ "subscriptions", "playlists", @@ -62,6 +64,8 @@ pub(crate) fn sync_capabilities() -> SyncCapabilities { fn collection_limit(collection: &str) -> HandlerResult { match collection { "settings" => Ok(2 * MEBIBYTE), + // Deprecated compatibility collection. Saved channel preferences now + // belong in `settings`; keep accepting this while old clients remain. "sessions" | "profiles" | "playbackSpeeds" => Ok(8 * MEBIBYTE), "subscriptions" | "playlistBookmarks" => Ok(16 * MEBIBYTE), "playlists" | "history" => Ok(MAX_ENCRYPTED_SYNC_BYTES), From 18fcf680120fc0f2e4b392951f02206b591177c1 Mon Sep 17 00:00:00 2001 From: D3SOX Date: Fri, 14 Aug 2026 09:10:31 +0200 Subject: [PATCH 2/4] fix(sync): deprecate playback speed error responses --- src/handlers/channel_playback_speeds.rs | 82 +++++++++++++++++++++---- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/src/handlers/channel_playback_speeds.rs b/src/handlers/channel_playback_speeds.rs index 16b9f4f..10c5f11 100644 --- a/src/handlers/channel_playback_speeds.rs +++ b/src/handlers/channel_playback_speeds.rs @@ -1,6 +1,15 @@ #![allow(deprecated)] -use actix_web::{HttpResponse, Responder, delete, get, middleware::from_fn, put, web}; +use actix_web::{ + HttpResponse, Responder, + body::MessageBody, + delete, + dev::{ServiceRequest, ServiceResponse}, + get, + http::header::{HeaderName, HeaderValue}, + middleware::{Next, from_fn}, + put, web, +}; use diesel_async::{AsyncConnection, scoped_futures::ScopedFutureExt}; use utoipa_actix_web::scope; @@ -20,7 +29,7 @@ use crate::{ models::{Account, ChannelPlaybackSpeed}, }; -const DEPRECATION_HEADER: (&str, &str) = ("Deprecation", "@1786665600"); +const DEPRECATION_DATE: &str = "@1786665600"; pub struct ChannelPlaybackSpeedsHandler; @@ -36,12 +45,25 @@ impl ScopedHandler for ChannelPlaybackSpeedsHandler { > { scope("/channel_playback_speeds") .wrap(from_fn(auth_middleware)) + .wrap(from_fn(deprecation_middleware)) .service(get_channel_playback_speeds) .service(put_channel_playback_speed) .service(delete_channel_playback_speed) } } +async fn deprecation_middleware( + req: ServiceRequest, + next: Next, +) -> Result, actix_web::Error> { + let mut response = next.call(req).await?; + response.headers_mut().insert( + HeaderName::from_static("deprecation"), + HeaderValue::from_static(DEPRECATION_DATE), + ); + Ok(response) +} + /// Get saved channel playback speeds through the deprecated dedicated API. /// /// New OpenTubeX clients sync saved channel preferences through the encrypted @@ -58,9 +80,7 @@ async fn get_channel_playback_speeds( .await .map_err(|_| HandlerError::InternalDatabaseError)?; - Ok(HttpResponse::Ok() - .insert_header(DEPRECATION_HEADER) - .json(speeds)) + Ok(HttpResponse::Ok().json(speeds)) } /// Save a channel playback speed through the deprecated dedicated API. @@ -87,9 +107,7 @@ async fn put_channel_playback_speed( let mut conn = get_db_conn!(pool); store_playback_speed(&mut conn, &speed).await?; - Ok(HttpResponse::Ok() - .insert_header(DEPRECATION_HEADER) - .json(speed)) + Ok(HttpResponse::Ok().json(speed)) } /// Store a playback speed, enforcing the row quota in the same transaction. @@ -138,7 +156,49 @@ async fn delete_channel_playback_speed( .await .map_err(|_| HandlerError::InternalDatabaseError)?; - Ok(HttpResponse::Ok() - .insert_header(DEPRECATION_HEADER) - .finish()) + Ok(HttpResponse::Ok().finish()) +} + +#[cfg(test)] +mod tests { + use actix_web::{App, HttpResponse, http::StatusCode, middleware::from_fn, test, web}; + + use super::{DEPRECATION_DATE, deprecation_middleware}; + use crate::handlers::{HandlerError, HandlerResult}; + + async fn success() -> HttpResponse { + HttpResponse::Ok().finish() + } + + async fn failure() -> HandlerResult { + Err(HandlerError::ValidationError) + } + + #[actix_web::test] + async fn deprecation_header_is_added_to_success_and_error_responses() { + let app = test::init_service( + App::new() + .wrap(from_fn(deprecation_middleware)) + .route("/success", web::get().to(success)) + .route("/failure", web::get().to(failure)), + ) + .await; + + for (path, expected_status) in [ + ("/success", StatusCode::OK), + ("/failure", StatusCode::BAD_REQUEST), + ] { + let response = + test::call_service(&app, test::TestRequest::get().uri(path).to_request()).await; + + assert_eq!(response.status(), expected_status); + assert_eq!( + response + .headers() + .get("deprecation") + .expect("deprecation header should be present"), + DEPRECATION_DATE + ); + } + } } From 7f3c93748963920232b964aa8aac92a03c1ce43e Mon Sep 17 00:00:00 2001 From: D3SOX Date: Fri, 14 Aug 2026 09:16:50 +0200 Subject: [PATCH 3/4] docs(sync): fix deprecation heading level --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7c3da3..ac05697 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ that may already exist. After that first upload, plaintext sync endpoints are rejected for the account so an older client cannot accidentally repopulate readable data. -### Deprecated playback-speed API +## Deprecated playback-speed API The dedicated `/v1/channel_playback_speeds` endpoints and encrypted `playbackSpeeds` collection are deprecated. Current OpenTubeX clients store all From a292e0131b105c9fc2f805b9656b1507bd78ce7b Mon Sep 17 00:00:00 2001 From: D3SOX Date: Fri, 14 Aug 2026 09:21:25 +0200 Subject: [PATCH 4/4] fix(sync): deprecate authentication error responses --- src/handlers/channel_playback_speeds.rs | 52 +++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/handlers/channel_playback_speeds.rs b/src/handlers/channel_playback_speeds.rs index 10c5f11..8482e44 100644 --- a/src/handlers/channel_playback_speeds.rs +++ b/src/handlers/channel_playback_speeds.rs @@ -1,10 +1,13 @@ #![allow(deprecated)] +use std::fmt::{Display, Formatter}; + use actix_web::{ HttpResponse, Responder, body::MessageBody, delete, dev::{ServiceRequest, ServiceResponse}, + error::ResponseError, get, http::header::{HeaderName, HeaderValue}, middleware::{Next, from_fn}, @@ -31,6 +34,27 @@ use crate::{ const DEPRECATION_DATE: &str = "@1786665600"; +#[derive(Debug)] +struct DeprecatedEndpointError(actix_web::Error); + +impl Display for DeprecatedEndpointError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, formatter) + } +} + +impl ResponseError for DeprecatedEndpointError { + fn status_code(&self) -> actix_web::http::StatusCode { + self.0.as_response_error().status_code() + } + + fn error_response(&self) -> HttpResponse { + let mut response = self.0.error_response(); + add_deprecation_header(response.headers_mut()); + response + } +} + pub struct ChannelPlaybackSpeedsHandler; impl ScopedHandler for ChannelPlaybackSpeedsHandler { @@ -56,12 +80,16 @@ async fn deprecation_middleware( req: ServiceRequest, next: Next, ) -> Result, actix_web::Error> { - let mut response = next.call(req).await?; - response.headers_mut().insert( + let mut response = next.call(req).await.map_err(DeprecatedEndpointError)?; + add_deprecation_header(response.headers_mut()); + Ok(response) +} + +fn add_deprecation_header(headers: &mut actix_web::http::header::HeaderMap) { + headers.insert( HeaderName::from_static("deprecation"), HeaderValue::from_static(DEPRECATION_DATE), ); - Ok(response) } /// Get saved channel playback speeds through the deprecated dedicated API. @@ -163,7 +191,7 @@ async fn delete_channel_playback_speed( mod tests { use actix_web::{App, HttpResponse, http::StatusCode, middleware::from_fn, test, web}; - use super::{DEPRECATION_DATE, deprecation_middleware}; + use super::{DEPRECATION_DATE, DeprecatedEndpointError, deprecation_middleware}; use crate::handlers::{HandlerError, HandlerResult}; async fn success() -> HttpResponse { @@ -174,6 +202,22 @@ mod tests { Err(HandlerError::ValidationError) } + #[actix_web::test] + async fn deprecation_header_is_added_when_inner_middleware_returns_an_error() { + let error: actix_web::Error = + DeprecatedEndpointError(HandlerError::InvalidToken.into()).into(); + let response = error.error_response(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!( + response + .headers() + .get("deprecation") + .expect("deprecation header should be present"), + DEPRECATION_DATE + ); + } + #[actix_web::test] async fn deprecation_header_is_added_to_success_and_error_responses() { let app = test::init_service(