Compare commits

..

7 Commits

Author SHA1 Message Date
Daniel Dietzler 49d5055a70 fix: oauth account linking 2026-07-06 11:25:44 +02:00
TJ Horner 103585ceb9 fix(web): add ARIA label to ServerStatisticsCard (#29497)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-06 10:35:47 +02:00
Daniel Dietzler 6da890ee37 fix: web search timeline visibility (#29518) 2026-07-03 17:41:25 +02:00
Lukas Schaefer e7ba8d3632 fix: live transcoding when viewing from a link share (#29471) 2026-07-03 11:34:37 -04:00
Lukas Schaefer 5645149e09 fix: downmix audio to stereo to fix broken channel layout (#29485)
* fix: downmix audio to stereo to fix broken channel layout

Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>

* add todo

Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>

---------

Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2026-07-03 10:55:12 -04:00
Alex 5a104f9dbe chore: update docs with v3 tags (#29312) 2026-07-03 12:27:18 +02:00
bo0tzz 13d17b3a11 fix: re-add gCastEnabled negation (#29499) 2026-07-03 11:05:22 +02:00
12 changed files with 46 additions and 36 deletions
-8
View File
@@ -681,10 +681,8 @@ class AlbumsApi {
/// Parameters:
///
/// * [String] id (required):
/// Album ID
///
/// * [String] userId (required):
/// Album user ID, or \"me\" to reference the current user
Future<Response> removeUserFromAlbumWithHttpInfo(String id, String userId, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/user/{userId}'
@@ -720,10 +718,8 @@ class AlbumsApi {
/// Parameters:
///
/// * [String] id (required):
/// Album ID
///
/// * [String] userId (required):
/// Album user ID, or \"me\" to reference the current user
Future<void> removeUserFromAlbum(String id, String userId, { Future<void>? abortTrigger, }) async {
final response = await removeUserFromAlbumWithHttpInfo(id, userId, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
@@ -802,10 +798,8 @@ class AlbumsApi {
/// Parameters:
///
/// * [String] id (required):
/// Album ID
///
/// * [String] userId (required):
/// Album user ID, or \"me\" to reference the current user
///
/// * [UpdateAlbumUserDto] updateAlbumUserDto (required):
Future<Response> updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future<void>? abortTrigger, }) async {
@@ -843,10 +837,8 @@ class AlbumsApi {
/// Parameters:
///
/// * [String] id (required):
/// Album ID
///
/// * [String] userId (required):
/// Album user ID, or \"me\" to reference the current user
///
/// * [UpdateAlbumUserDto] updateAlbumUserDto (required):
Future<void> updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future<void>? abortTrigger, }) async {
-4
View File
@@ -2702,7 +2702,6 @@
"name": "id",
"required": true,
"in": "path",
"description": "Album ID",
"schema": {
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -2713,7 +2712,6 @@
"name": "userId",
"required": true,
"in": "path",
"description": "Album user ID, or \"me\" to reference the current user",
"schema": {
"type": "string"
}
@@ -2764,7 +2762,6 @@
"name": "id",
"required": true,
"in": "path",
"description": "Album ID",
"schema": {
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -2775,7 +2772,6 @@
"name": "userId",
"required": true,
"in": "path",
"description": "Album user ID, or \"me\" to reference the current user",
"schema": {
"type": "string"
}
+8 -4
View File
@@ -7,7 +7,6 @@ import {
AlbumsAddAssetsDto,
AlbumsAddAssetsResponseDto,
AlbumStatisticsResponseDto,
AlbumUserParamDto,
CreateAlbumDto,
GetAlbumsDto,
UpdateAlbumDto,
@@ -19,7 +18,7 @@ import { MapMarkerResponseDto } from 'src/dtos/map.dto';
import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AlbumService } from 'src/services/album.service';
import { UUIDParamDto } from 'src/validation';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ApiTags(ApiTag.Albums)
@Controller('albums')
@@ -176,7 +175,8 @@ export class AlbumController {
})
updateAlbumUser(
@Auth() auth: AuthDto,
@Param() { id, userId }: AlbumUserParamDto,
@Param() { id }: UUIDParamDto,
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
@Body() dto: UpdateAlbumUserDto,
): Promise<void> {
return this.service.updateUser(auth, id, userId, dto);
@@ -190,7 +190,11 @@ export class AlbumController {
description: 'Remove a user from an album. Use an ID of "me" to leave a shared album.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
})
removeUserFromAlbum(@Auth() auth: AuthDto, @Param() { id, userId }: AlbumUserParamDto): Promise<void> {
removeUserFromAlbum(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
): Promise<void> {
return this.service.removeUser(auth, id, userId);
}
}
-12
View File
@@ -140,18 +140,6 @@ export const AlbumResponseSchema = z
})
.meta({ id: 'AlbumResponseDto' });
const AlbumUserParamSchema = z.object({
id: z.uuidv4().describe('Album ID'),
// TODO: disallow 'me' as a shortcut in v4 and type userId as uuidv4
userId: z
.string()
.refine((value) => value === 'me' || z.uuidv4().safeParse(value).success, {
error: 'Must be a UUID v4 or "me"',
})
.describe('Album user ID, or "me" to reference the current user'),
});
export class AlbumUserParamDto extends createZodDto(AlbumUserParamSchema) {}
export class AddUsersDto extends createZodDto(AddUsersSchema) {}
export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {}
export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {}
+1 -1
View File
@@ -426,7 +426,7 @@ export class AuthService extends BaseService {
throw new BadRequestException('This OAuth account has already been linked to another user.');
}
if (auth.session) {
if (auth.session && sid) {
await this.sessionRepository.update(auth.session.id, { oauthSid: sid });
}
@@ -381,6 +381,8 @@ describe(TranscodingService.name, () => {
'50',
'-keyint_min',
'50',
'-ac',
'2',
'-copyts',
'-r',
'50130000/2012441',
+5
View File
@@ -200,6 +200,11 @@ export class BaseConfig implements VideoCodecSWConfig {
const options = ['-c:v', videoCodec, '-c:a', audioCodec, '-map', `0:${videoStream.index}`, '-map_metadata', '-1'];
if (audioStream) {
options.push('-map', `0:${audioStream.index}`);
// If there are more than 2 channels sometimes the channel config is broken when re-encoded
// TODO: Store the number of channels in the db and then set it during the transcoding: -channel_layout 5.1
if ([TranscodeTarget.All, TranscodeTarget.Audio].includes(target)) {
options.push('-ac', '2');
}
}
if (this.getBFrames() > -1) {
options.push('-bf', `${this.getBFrames()}`);
+11 -1
View File
@@ -1,4 +1,4 @@
import { FileValidator, Injectable } from '@nestjs/common';
import { ArgumentMetadata, FileValidator, Injectable, ParseUUIDPipe } from '@nestjs/common';
import { createZodDto } from 'nestjs-zod';
import sanitize from 'sanitize-filename';
import { isIP, isIPRange } from 'validator';
@@ -74,6 +74,16 @@ export function IsNotSiblingOf<
);
}
@Injectable()
export class ParseMeUUIDPipe extends ParseUUIDPipe {
async transform(value: string, metadata: ArgumentMetadata) {
if (value == 'me') {
return value;
}
return super.transform(value, metadata);
}
}
@Injectable()
export class FileNotEmptyValidator extends FileValidator {
constructor(private requiredFields: string[]) {
@@ -5,6 +5,7 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { castManager } from '$lib/managers/cast-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte';
import { autoPlayVideo, lang, loopVideo as loopVideoPreference } from '$lib/stores/preferences.store';
import { getAssetHlsSessionUrl, getAssetHlsUrl, getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils';
@@ -150,6 +151,15 @@
},
},
useMediaCapabilities: false,
xhrSetup: (xhr: XMLHttpRequest, url: string) => {
const authenticatedUrl = new URL(url, globalThis.location.origin);
for (const [key, value] of Object.entries(authManager.params)) {
if (value) {
authenticatedUrl.searchParams.set(key, value as string);
}
}
xhr.open('GET', authenticatedUrl.toString());
},
};
const releaseSession = () => {
@@ -37,18 +37,18 @@
</div>
{#await valuePromise}
<div class="relative mx-auto font-mono text-2xl font-medium">
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="0">
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros()}</span>
</div>
{:then data}
<div class="relative mx-auto font-mono text-2xl font-medium">
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="{data.value} {data.unit ?? ''}">
<span class="text-gray-300 dark:text-gray-600">{zeros(data)}</span><span>{data.value}</span>
{#if data.unit}
<code class="font-mono text-base font-normal">{data.unit}</code>
{/if}
</div>
{:catch _}
<div class="relative mx-auto font-mono text-2xl font-medium">
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="0">
<span class="text-gray-300 dark:text-gray-600">{zeros()}</span>
</div>
{/await}
@@ -25,7 +25,7 @@ export class GCastDestination implements ICastDestination {
private currentUrl: string | null = null;
async initialize(): Promise<boolean> {
if (!authManager.authenticated || authManager.preferences.cast.gCastEnabled) {
if (!authManager.authenticated || !authManager.preferences.cast.gCastEnabled) {
this.isAvailable = false;
return false;
}
@@ -34,6 +34,7 @@
import {
type AlbumResponseDto,
type AssetResponseDto,
AssetVisibility,
getPerson,
getTagById,
type MetadataSearchDto,
@@ -141,8 +142,10 @@
try {
const { albums, assets } =
('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
? await searchSmart({ smartSearchDto: { ...searchDto, language: $lang } })
: await searchAssets({ metadataSearchDto: searchDto });
? await searchSmart({
smartSearchDto: { visibility: AssetVisibility.Timeline, ...searchDto, language: $lang },
})
: await searchAssets({ metadataSearchDto: { visibility: AssetVisibility.Timeline, ...searchDto } });
searchResultAlbums.push(...albums.items);
searchResultAssets.push(...assets.items);