mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat(auth): add SessionExpiredBanner to all remaining web apps
Added to: clock, photos, storage, mukke, planta, picture, skilltree, nutriphi, chat. Now all 13 web apps show a re-login banner when token refresh permanently fails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90c438e267
commit
bf7517d24d
23 changed files with 842 additions and 19 deletions
|
|
@ -70,6 +70,59 @@ describe('parseSongInput', () => {
|
|||
const result = parseSongInput('Track 5bpm'); // too low
|
||||
expect(result.bpm).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract album from parentheses', () => {
|
||||
const result = parseSongInput('Queen - Bohemian Rhapsody (A Night at the Opera)');
|
||||
expect(result.artist).toBe('Queen');
|
||||
expect(result.title).toBe('Bohemian Rhapsody');
|
||||
expect(result.album).toBe('A Night at the Opera');
|
||||
});
|
||||
|
||||
it('should extract album from title without artist', () => {
|
||||
const result = parseSongInput('Song (Album)');
|
||||
expect(result.title).toBe('Song');
|
||||
expect(result.album).toBe('Album');
|
||||
});
|
||||
|
||||
it('should detect multi-artist with ft.', () => {
|
||||
const result = parseSongInput('Daft Punk ft. Pharrell - Get Lucky');
|
||||
expect(result.artist).toBe('Daft Punk');
|
||||
expect(result.artists).toEqual(['Daft Punk', 'Pharrell']);
|
||||
expect(result.title).toBe('Get Lucky');
|
||||
});
|
||||
|
||||
it('should detect multi-artist with &', () => {
|
||||
const result = parseSongInput('ACDC & Brian Johnson - Thunderstruck');
|
||||
expect(result.artist).toBe('ACDC');
|
||||
expect(result.artists).toEqual(['ACDC', 'Brian Johnson']);
|
||||
expect(result.title).toBe('Thunderstruck');
|
||||
});
|
||||
|
||||
it('should detect multi-artist with feat.', () => {
|
||||
const result = parseSongInput('Jay-Z feat. Kanye West - Niggas in Paris');
|
||||
expect(result.artist).toBe('Jay-Z');
|
||||
expect(result.artists).toEqual(['Jay-Z', 'Kanye West']);
|
||||
});
|
||||
|
||||
it('should detect multi-artist with featuring', () => {
|
||||
const result = parseSongInput('Eminem featuring Rihanna - Love the Way You Lie');
|
||||
expect(result.artist).toBe('Eminem');
|
||||
expect(result.artists).toEqual(['Eminem', 'Rihanna']);
|
||||
});
|
||||
|
||||
it('should not set artists for single artist', () => {
|
||||
const result = parseSongInput('Queen - Bohemian Rhapsody');
|
||||
expect(result.artist).toBe('Queen');
|
||||
expect(result.artists).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should combine album and multi-artist', () => {
|
||||
const result = parseSongInput('Daft Punk ft. Pharrell - Get Lucky (Random Access Memories)');
|
||||
expect(result.artist).toBe('Daft Punk');
|
||||
expect(result.artists).toEqual(['Daft Punk', 'Pharrell']);
|
||||
expect(result.title).toBe('Get Lucky');
|
||||
expect(result.album).toBe('Random Access Memories');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatParsedSongPreview', () => {
|
||||
|
|
@ -97,6 +150,12 @@ describe('formatParsedSongPreview', () => {
|
|||
expect(preview).toContain('Neue Playlist');
|
||||
});
|
||||
|
||||
it('should format album', () => {
|
||||
const parsed = parseSongInput('Queen - Bohemian Rhapsody (A Night at the Opera)');
|
||||
const preview = formatParsedSongPreview(parsed);
|
||||
expect(preview).toContain('💿 A Night at the Opera');
|
||||
});
|
||||
|
||||
it('should return empty for simple title', () => {
|
||||
const parsed = parseSongInput('Simple Song');
|
||||
expect(formatParsedSongPreview(parsed)).toBe('');
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { extractTags, type ParserLocale } from '@manacore/shared-utils';
|
|||
export interface ParsedSong {
|
||||
title: string;
|
||||
artist?: string;
|
||||
artists?: string[];
|
||||
album?: string;
|
||||
genre?: string;
|
||||
bpm?: number;
|
||||
|
|
@ -53,6 +54,12 @@ const PROJECT_PATTERNS_BY_LOCALE: Record<ParserLocale, RegExp[]> = {
|
|||
it: [/\bnuovo\s+progetto\b/i, /\bprogetto\b/i],
|
||||
};
|
||||
|
||||
// Album pattern: trailing parenthesized text e.g. "Title (Album Name)"
|
||||
const ALBUM_PATTERN = /\(([^)]+)\)\s*$/;
|
||||
|
||||
// Multi-artist separator patterns
|
||||
const MULTI_ARTIST_PATTERN = /\s+(?:ft\.?|feat\.?|featuring|&|x|vs\.?)\s+/i;
|
||||
|
||||
// "Artist - Title" separator
|
||||
const ARTIST_TITLE_SEPARATOR = /\s+[-–—]\s+/;
|
||||
|
||||
|
|
@ -67,6 +74,21 @@ function extractBpm(text: string): { bpm?: number; remaining: string } {
|
|||
return { bpm: undefined, remaining: text };
|
||||
}
|
||||
|
||||
function extractAlbum(text: string): { album?: string; remaining: string } {
|
||||
const match = text.match(ALBUM_PATTERN);
|
||||
if (match) {
|
||||
return { album: match[1].trim(), remaining: text.replace(ALBUM_PATTERN, '').trim() };
|
||||
}
|
||||
return { album: undefined, remaining: text };
|
||||
}
|
||||
|
||||
function extractArtists(artist: string): string[] {
|
||||
return artist
|
||||
.split(MULTI_ARTIST_PATTERN)
|
||||
.map((a) => a.trim())
|
||||
.filter((a) => a.length > 0);
|
||||
}
|
||||
|
||||
function extractYear(text: string): { year?: number; remaining: string } {
|
||||
const match = text.match(YEAR_PATTERN);
|
||||
if (match) {
|
||||
|
|
@ -124,6 +146,10 @@ export function parseSongInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
const typeResult = extractTypeKeyword(text, locale);
|
||||
text = typeResult.remaining;
|
||||
|
||||
// Extract album from parentheses (before other extractions to avoid confusion)
|
||||
const albumResult = extractAlbum(text);
|
||||
text = albumResult.remaining;
|
||||
|
||||
// Extract BPM
|
||||
const bpmResult = extractBpm(text);
|
||||
text = bpmResult.remaining;
|
||||
|
|
@ -134,12 +160,22 @@ export function parseSongInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
|
||||
// Try "Artist - Title" format
|
||||
let artist: string | undefined;
|
||||
let artists: string[] | undefined;
|
||||
let title: string;
|
||||
|
||||
if (ARTIST_TITLE_SEPARATOR.test(text)) {
|
||||
const parts = text.split(ARTIST_TITLE_SEPARATOR, 2);
|
||||
artist = parts[0].trim();
|
||||
const rawArtist = parts[0].trim();
|
||||
title = parts[1].trim();
|
||||
|
||||
// Detect multi-artist patterns
|
||||
const artistList = extractArtists(rawArtist);
|
||||
if (artistList.length > 1) {
|
||||
artist = artistList[0];
|
||||
artists = artistList;
|
||||
} else {
|
||||
artist = rawArtist;
|
||||
}
|
||||
} else {
|
||||
title = text.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
|
@ -147,6 +183,8 @@ export function parseSongInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
return {
|
||||
title,
|
||||
artist,
|
||||
artists,
|
||||
album: albumResult.album,
|
||||
genre,
|
||||
bpm: bpmResult.bpm,
|
||||
year: yearResult.year,
|
||||
|
|
@ -173,6 +211,10 @@ export function formatParsedSongPreview(parsed: ParsedSong, locale: ParserLocale
|
|||
parts.push(`🎤 ${parsed.artist}`);
|
||||
}
|
||||
|
||||
if (parsed.album) {
|
||||
parts.push(`💿 ${parsed.album}`);
|
||||
}
|
||||
|
||||
if (parsed.genre) {
|
||||
parts.push(`🎵 ${parsed.genre}`);
|
||||
}
|
||||
|
|
|
|||
48
apps/mukke/apps/web/src/lib/utils/syntax-help.ts
Normal file
48
apps/mukke/apps/web/src/lib/utils/syntax-help.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Mukke-specific syntax help patterns
|
||||
*/
|
||||
import type { SyntaxGroup } from '@manacore/shared-ui';
|
||||
|
||||
export const MUKKE_SYNTAX: SyntaxGroup[] = [
|
||||
{
|
||||
title: 'Musik',
|
||||
items: [
|
||||
{
|
||||
pattern: 'Artist - Title',
|
||||
description: 'Interpret und Titel mit Bindestrich trennen',
|
||||
examples: ['Queen - Bohemian Rhapsody', 'Daft Punk ft. Pharrell - Get Lucky'],
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
pattern: '(Album)',
|
||||
description: 'Album in Klammern',
|
||||
examples: ['Queen - Song (A Night at the Opera)'],
|
||||
color: 'accent',
|
||||
},
|
||||
{
|
||||
pattern: '#genre',
|
||||
description: 'Genre als Tag',
|
||||
examples: ['#rock', '#electronic', '#jazz'],
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
pattern: 'BPM',
|
||||
description: 'Tempo in BPM',
|
||||
examples: ['120bpm', '90 BPM'],
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
pattern: 'Neue Playlist',
|
||||
description: 'Playlist erstellen',
|
||||
examples: ['Neue Playlist Workout', 'Playlist Chill'],
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
pattern: 'Neues Projekt',
|
||||
description: 'Editor-Projekt erstellen',
|
||||
examples: ['Neues Projekt Demo', 'Projekt Remix'],
|
||||
color: 'success',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue