feat(playlists): share playlists with revocable read-only links
ci / api / api (push) Successful in 55s
release / release (версия и заметки) (push) Successful in 5s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m21s
ci / ts / ts (packages + web) (push) Successful in 2m19s
release / android (APK в релиз) (push) Successful in 4m45s
release / desktop (Linux и Windows в релиз) (push) Successful in 20m58s
release / prune (старые файлы и прогоны) (push) Successful in 9s
ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m35s
ci / db / db (push) Successful in 59s
ci / api / api (push) Successful in 55s
release / release (версия и заметки) (push) Successful in 5s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m21s
ci / ts / ts (packages + web) (push) Successful in 2m19s
release / android (APK в релиз) (push) Successful in 4m45s
release / desktop (Linux и Windows в релиз) (push) Successful in 20m58s
release / prune (старые файлы и прогоны) (push) Successful in 9s
ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m35s
ci / db / db (push) Successful in 59s
This commit is contained in:
1 parent
bfcf6e48b2
commit
0a59059738
23 files changed
+1164
-319
No files matched your search
@@ -1439,10 +1439,53 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/me/playlists/{id}/share": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Создать или вернуть ссылку только для чтения (только владелец) */
|
||||
post: operations["sharePlaylist"];
|
||||
/** Отозвать ссылку (только владелец); повторная публикация создаёт новую ссылку */
|
||||
delete: operations["revokePlaylistShare"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/shared/playlists/{shareId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
shareId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
/** Прочитать плейлист по действующей ссылке (требуется вход) */
|
||||
get: operations["getSharedPlaylist"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
PlaylistShare: {
|
||||
/** Format: uuid */
|
||||
share_id: string;
|
||||
};
|
||||
PlaylistInput: {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -1466,6 +1509,11 @@ export interface components {
|
||||
updated_at: string;
|
||||
};
|
||||
Playlist: components["schemas"]["PlaylistSummary"] & {
|
||||
/**
|
||||
* Format: uuid
|
||||
* @description Действующая ссылка; возвращается только владельцу через /me/playlists
|
||||
*/
|
||||
readonly share_id?: string | null;
|
||||
tracks: components["schemas"]["Track"][];
|
||||
};
|
||||
PlaylistList: {
|
||||
@@ -4607,4 +4655,71 @@ export interface operations {
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
sharePlaylist: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Идентификатор ссылки для /shared/playlist/{shareId}; для открытия требуется вход */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PlaylistShare"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
revokePlaylistShare: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Доступ по прежней ссылке отключён */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
getSharedPlaylist: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
shareId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Плейлист с реакциями и ссылками на стрим для текущего слушателя */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Playlist"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export {
|
||||
type Track,
|
||||
} from './player';
|
||||
export { normalizeGain, TARGET_LUFS } from './gain';
|
||||
export { trackShareUrl } from './share';
|
||||
export { playlistShareUrl, trackShareUrl } from './share';
|
||||
export { checkServer, normalizeServerUrl, type ServerCheck } from './server';
|
||||
export { initialWaveState, mix, rgb, rgba, step, WAVE_COLORS, wavePalette, waveScale, type Rgb, type WaveState } from './wave-anim';
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { trackShareUrl } from './share';
|
||||
import { playlistShareUrl, trackShareUrl } from './share';
|
||||
|
||||
describe('trackShareUrl', () => {
|
||||
it('builds a page URL without stream credentials', () => {
|
||||
expect(trackShareUrl('https://music.example', 'ab/cd')).toBe('https://music.example/track/ab%2Fcd');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('builds a shared playlist URL from the server without stream credentials', () => {
|
||||
expect(playlistShareUrl('https://music.example/api/v1', 'ab/cd')).toBe('https://music.example/shared/playlist/ab%2Fcd');
|
||||
});
|
||||
@@ -2,3 +2,8 @@
|
||||
export function trackShareUrl(server: string, trackId: string): string {
|
||||
return new URL(`/track/${encodeURIComponent(trackId)}`, server).href;
|
||||
}
|
||||
|
||||
/** Revocable read-only playlist page; requires a listener account on this server. */
|
||||
export function playlistShareUrl(server: string, shareId: string): string {
|
||||
return new URL(`/shared/playlist/${encodeURIComponent(shareId)}`, server).href;
|
||||
}
|
||||
@@ -340,6 +340,11 @@
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"share": "Share playlist",
|
||||
"link_copied": "Playlist link copied",
|
||||
"shared": "Shared by link",
|
||||
"revoke_share": "Disable link access",
|
||||
"share_hint": "Anyone with an account on this server and this link can listen. Only you can edit the playlist.",
|
||||
"name": "Name",
|
||||
"delete_confirm": "Delete playlist “{{name}}”? Tracks will remain in your library.",
|
||||
"manage": "Manage tracks",
|
||||
|
||||
@@ -340,6 +340,11 @@
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"share": "Поделиться плейлистом",
|
||||
"link_copied": "Ссылка на плейлист скопирована",
|
||||
"shared": "Доступ по ссылке",
|
||||
"revoke_share": "Отключить доступ по ссылке",
|
||||
"share_hint": "Любой пользователь этого сервера со ссылкой сможет слушать плейлист. Изменять его можете только вы.",
|
||||
"name": "Название",
|
||||
"delete_confirm": "Удалить плейлист «{{name}}»? Треки останутся в библиотеке.",
|
||||
"manage": "Управлять треками",
|
||||
|
||||
Reference in new issue
Block a user