diff --git a/backend/endpoints/responses/music.py b/backend/endpoints/responses/music.py index 5ee7b0aa51..7c8d23ff0a 100644 --- a/backend/endpoints/responses/music.py +++ b/backend/endpoints/responses/music.py @@ -3,7 +3,7 @@ from typing import Any from urllib.parse import quote -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from config import FRONTEND_RESOURCES_PATH @@ -26,11 +26,14 @@ class MusicTrackSchema(BaseModel): has_embedded_cover: bool = False is_favorite: bool = False game_name: str | None = None + game_genres: list[str] = Field(default_factory=list) + added_at: UTCDatetime platform_id: int platform_slug: str platform_name: str stream_url: str cover_url: str | None = None + game_cover_url: str | None = None @staticmethod def cover_url_for(cover_path: str | None, rom_cover_path: str | None) -> str | None: @@ -55,11 +58,18 @@ def from_row(cls, row: Any) -> MusicTrackSchema: has_embedded_cover=row.has_embedded_cover, is_favorite=bool(row.is_favorite), game_name=row.game_name, + game_genres=row.game_genres or [], + added_at=row.added_at, platform_id=row.platform_id, platform_slug=row.platform_slug, platform_name=row.platform_name, stream_url=f"/api/roms/{row.rom_file_id}/files/content/{quote(row.file_name)}", cover_url=cls.cover_url_for(row.cover_path, row.path_cover_l), + game_cover_url=( + f"{FRONTEND_RESOURCES_PATH}/{row.path_cover_l}" + if row.path_cover_l + else None + ), ) diff --git a/backend/endpoints/responses/permission.py b/backend/endpoints/responses/permission.py index b050535392..84007b4604 100644 --- a/backend/endpoints/responses/permission.py +++ b/backend/endpoints/responses/permission.py @@ -41,6 +41,8 @@ class ActionKey(enum.StrEnum): COLLECTION_CREATE = "collection.create" COLLECTION_EDIT = "collection.edit" COLLECTION_DELETE = "collection.delete" + # Playlists + PLAYLIST_EDIT = "playlist.edit" # Library LIBRARY_SCAN = "library.scan" # Users @@ -83,6 +85,7 @@ class ActionKey(enum.StrEnum): ActionKey.COLLECTION_EDIT, ), (PermEntity.COLLECTIONS, PermAction.DELETE): (ActionKey.COLLECTION_DELETE,), + (PermEntity.PLAYLISTS, PermAction.WRITE): (ActionKey.PLAYLIST_EDIT,), (PermEntity.USERS, PermAction.READ): (ActionKey.USER_VIEW,), (PermEntity.USERS, PermAction.WRITE): ( ActionKey.USER_CREATE, diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index a6528571fb..aeb13016ca 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -2353,6 +2353,8 @@ def get_music_tracks( is_favorite_col.label("is_favorite"), Rom.name.label("game_name"), Rom.path_cover_l.label("path_cover_l"), + RomMetadata.genres.label("game_genres"), + RomFile.created_at.label("added_at"), Platform.id.label("platform_id"), Platform.slug.label("platform_slug"), Platform.name.label("platform_name"), @@ -2360,6 +2362,7 @@ def get_music_tracks( .select_from(TrackMeta) .join(RomFile, TrackMeta.rom_file_id == RomFile.id) .join(Rom, TrackMeta.rom_id == Rom.id) + .outerjoin(RomMetadata, RomMetadata.rom_id == Rom.id) .join(Platform, Rom.platform_id == Platform.id) ) if is_favorite_user_id is not None: diff --git a/frontend/src/__generated__/models/ActionKey.ts b/frontend/src/__generated__/models/ActionKey.ts index 0b9bf2a9af..c4132bdfd1 100644 --- a/frontend/src/__generated__/models/ActionKey.ts +++ b/frontend/src/__generated__/models/ActionKey.ts @@ -2,4 +2,4 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type ActionKey = 'rom.view' | 'rom.play' | 'rom.download' | 'rom.upload' | 'rom.edit' | 'rom.delete' | 'rom.match' | 'rom.refresh' | 'rom.favorite' | 'platform.view' | 'platform.create' | 'platform.edit' | 'platform.delete' | 'collection.view' | 'collection.create' | 'collection.edit' | 'collection.delete' | 'library.scan' | 'user.view' | 'user.create' | 'user.edit' | 'user.delete' | 'app.admin'; +export type ActionKey = 'rom.view' | 'rom.play' | 'rom.download' | 'rom.upload' | 'rom.edit' | 'rom.delete' | 'rom.match' | 'rom.refresh' | 'rom.favorite' | 'platform.view' | 'platform.create' | 'platform.edit' | 'platform.delete' | 'collection.view' | 'collection.create' | 'collection.edit' | 'collection.delete' | 'playlist.edit' | 'library.scan' | 'user.view' | 'user.create' | 'user.edit' | 'user.delete' | 'app.admin'; diff --git a/frontend/src/__generated__/models/MusicTrackSchema.ts b/frontend/src/__generated__/models/MusicTrackSchema.ts index d3a7d844c4..4a973aa6b7 100644 --- a/frontend/src/__generated__/models/MusicTrackSchema.ts +++ b/frontend/src/__generated__/models/MusicTrackSchema.ts @@ -16,10 +16,13 @@ export type MusicTrackSchema = { has_embedded_cover?: boolean; is_favorite?: boolean; game_name?: (string | null); + game_genres?: Array; + added_at: string; platform_id: number; platform_slug: string; platform_name: string; stream_url: string; cover_url?: (string | null); + game_cover_url?: (string | null); }; diff --git a/frontend/src/locales/bg_BG/common.json b/frontend/src/locales/bg_BG/common.json index 9984b40aad..9933020682 100644 --- a/frontend/src/locales/bg_BG/common.json +++ b/frontend/src/locales/bg_BG/common.json @@ -8,11 +8,13 @@ "activity": "Активност", "add": "Добави", "administration": "Администрация", + "albums-n": "{n} албум | {n} албума", "all": "Всички", "all-files-skipped": "Всички файлове са пропуснати, нищо не е качено.", "all-platforms": "Всички платформи", "and": "и", "apply": "Приложи", + "artists-n": "{n} изпълнител | {n} изпълнители", "ascii-only": "Само ASCII символи", "back": "Назад", "back-btn": "Назад", @@ -42,6 +44,8 @@ "core": "Основни", "create": "Създай", "crt-mode": "CRT mode", + "decade-mix": "Микс по десетилетия", + "decades-n": "{n} десетилетие | {n} десетилетия", "delete": "Изтрий", "details": "Детайли", "discard": "Отхвърли", @@ -54,20 +58,24 @@ "edit": "Редактирай", "exclude-on-delete": "Не добавяй повторно след изтриване", "expand": "Разшири", + "favorite-soundtracks": "Любими саундтраци", "filter": "Филтър", "firmware": "Фърмуер", "first-page": "Първа страница", "for": "за", + "free-radio": "Свободно радио", "games": "Игри", "games-header": "Игри", "games-n": "{n} игра | {n} игри", "generate": "Генерирай", + "genres-n": "{n} жанр | {n} жанра", "got-it": "Разбрах", "hide-password": "Скрий паролата", "home": "Начало", "in-library": "В библиотеката", "invalid-email": "Невалиден имейл", "invalid-name": "Невалидно име", + "jukebox": "Музикален плеър", "kind": "Вид", "last-page": "Последна страница", "last-updated": "Последна промяна", @@ -79,6 +87,9 @@ "local": "Локален", "logout": "Изход", "logs": "Logs", + "minutes-n": "{n} мин", + "music-by-album": "Саундтраци по албум", + "music-by-artist": "Саундтраци по изпълнител", "name": "Име", "new-version-available": "Налична е нова версия", "next-page": "Следваща страница", @@ -95,10 +106,13 @@ "platform-created": "Платформата {name} е създадена", "platforms": "Платформи", "platforms-n": "{n} платформа | {n} платформи", + "play-all": "Пусни всички", + "playlists": "Плейлисти", "previous-page": "Предишна страница", "primary-navigation": "Основна навигация", "profile": "Профил", "random": "Случайно", + "recently-added-soundtracks": "Наскоро добавени саундтраци", "remove": "Премахни", "removing-from-filesystem": "Премахване от файловата система", "required": "Задължително", @@ -116,11 +130,17 @@ "server-stats": "Статистики на сървъра", "settings": "Настройки", "show-password": "Покажи паролата", + "shuffle": "Разбъркване", "size": "Размер", "size-on-disk": "Размер на диска", "slug": "Слъг", "smart-collection": "Умна колекция", "smart-collections": "Умни колекции", + "soundtrack-favorite-added": "Песента е добавена към любимите", + "soundtrack-favorite-failed": "Неуспешна промяна на любимата песен", + "soundtrack-favorite-removed": "Песента е премахната от любимите", + "soundtracks-by-genre": "Саундтраци по жанр", + "soundtracks-by-platform": "Саундтраци по платформа", "state": "Състояние", "states": "Състояния", "states-n": "{n} състояние | {n} състояния", diff --git a/frontend/src/locales/cs_CZ/common.json b/frontend/src/locales/cs_CZ/common.json index 0d2a13a9d8..33b9786eb6 100644 --- a/frontend/src/locales/cs_CZ/common.json +++ b/frontend/src/locales/cs_CZ/common.json @@ -8,11 +8,13 @@ "activity": "Aktivita", "add": "Přidat", "administration": "Administrace", + "albums-n": "{n} album | {n} alba", "all": "Vše", "all-files-skipped": "Všechny soubory přeskočeny, nic nebylo nahráno.", "all-platforms": "Všechny platformy", "and": "a", "apply": "Použít", + "artists-n": "{n} interpret | {n} interpreti", "ascii-only": "Pouze ASCII znaky", "back": "Zpět", "back-btn": "Zpět", @@ -42,6 +44,8 @@ "core": "Jádro", "create": "Vytvořit", "crt-mode": "CRT mode", + "decade-mix": "Mix desetiletí", + "decades-n": "{n} desetiletí | {n} desetiletí", "delete": "Smazat", "details": "Detaily", "discard": "Zahodit", @@ -54,20 +58,24 @@ "edit": "Upravit", "exclude-on-delete": "Vynechat při mazání", "expand": "Rozbalit", + "favorite-soundtracks": "Oblíbené soundtracky", "filter": "Filtr", "firmware": "Firmware", "first-page": "První stránka", "for": "pro", + "free-radio": "Volné rádio", "games": "Hry", "games-header": "Hry", "games-n": "{n} her | {n} hra | {n} hry | {n} her", "generate": "Vygenerovat", + "genres-n": "{n} žánr | {n} žánry", "got-it": "Rozumím", "hide-password": "Skrýt heslo", "home": "Domů", "in-library": "V knihovně", "invalid-email": "Neplatný e-mail", "invalid-name": "Neplatný název", + "jukebox": "Hudební přehrávač", "kind": "Druh", "last-page": "Poslední stránka", "last-updated": "Naposledy aktualizováno", @@ -79,6 +87,9 @@ "local": "Lokální", "logout": "Odhlásit se", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Soundtracky podle alba", + "music-by-artist": "Soundtracky podle interpreta", "name": "Název", "new-version-available": "Je k dispozici nová verze", "next-page": "Další stránka", @@ -94,11 +105,14 @@ "platform": "Platforma", "platform-created": "Platforma {name} vytvořena", "platforms": "Platformy", - "platforms-n": "{n} platforem | {n} platforma | {n} platformy | {n} platforem", + "platforms-n": "{n} platforma | {n} platformy", + "play-all": "Přehrát vše", + "playlists": "Playlisty", "previous-page": "Předchozí stránka", "primary-navigation": "Hlavní navigace", "profile": "Profil", "random": "Náhodně", + "recently-added-soundtracks": "Nedávno přidané soundtracky", "remove": "Odebrat", "removing-from-filesystem": "Odstraňování ze systému", "required": "Povinné", @@ -116,11 +130,17 @@ "server-stats": "Statistiky serveru", "settings": "Nastavení", "show-password": "Zobrazit heslo", + "shuffle": "Náhodné pořadí", "size": "Velikost", "size-on-disk": "Velikost na disku", "slug": "Slug", "smart-collection": "Chytrá kolekce", "smart-collections": "Chytré kolekce", + "soundtrack-favorite-added": "Skladba byla přidána do oblíbených", + "soundtrack-favorite-failed": "Oblíbenou skladbu se nepodařilo změnit", + "soundtrack-favorite-removed": "Skladba byla odebrána z oblíbených", + "soundtracks-by-genre": "Soundtracky podle žánru", + "soundtracks-by-platform": "Soundtracky podle platformy", "state": "Stav", "states": "Stavy", "states-n": "{n} stavů | {n} stav | {n} stavy | {n} stavů", diff --git a/frontend/src/locales/de_DE/common.json b/frontend/src/locales/de_DE/common.json index cbc24a08f6..80590f5413 100644 --- a/frontend/src/locales/de_DE/common.json +++ b/frontend/src/locales/de_DE/common.json @@ -8,11 +8,13 @@ "activity": "Aktivität", "add": "Hinzufügen", "administration": "Administration", + "albums-n": "{n} Album | {n} Alben", "all": "Alle", "all-files-skipped": "Alle Dateien übersprungen, nichts hochgeladen.", "all-platforms": "Alle Plattformen", "and": "und", "apply": "Anwenden", + "artists-n": "{n} Künstler | {n} Künstler", "ascii-only": "Nur ASCII-Zeichen", "back": "Zurück", "back-btn": "Zurück", @@ -42,6 +44,8 @@ "core": "Core", "create": "Erstellen", "crt-mode": "CRT-Modus", + "decade-mix": "Jahrzehnte-Mix", + "decades-n": "{n} Jahrzehnt | {n} Jahrzehnte", "delete": "Löschen", "details": "Details", "discard": "Verwerfen", @@ -54,20 +58,24 @@ "edit": "Bearbeiten", "exclude-on-delete": "Beim Löschen ausschließen", "expand": "Ausklappen", + "favorite-soundtracks": "Favorisierte Soundtracks", "filter": "Filter", "firmware": "Firmware", "first-page": "Erste Seite", "for": "für", + "free-radio": "Freies Radio", "games": "Spiele", "games-header": "Spiele", "games-n": "{n} Spiel | {n} Spiele", "generate": "Generieren", + "genres-n": "{n} Genre | {n} Genres", "got-it": "Verstanden", "hide-password": "Passwort ausblenden", "home": "Startseite", "in-library": "In der Bibliothek", "invalid-email": "Ungültige E-Mail", "invalid-name": "Ungültiger Name", + "jukebox": "Musikbox", "kind": "Typ", "last-page": "Letzte Seite", "last-updated": "Zuletzt aktualisiert", @@ -79,6 +87,9 @@ "local": "Lokal", "logout": "Ausloggen", "logs": "Logs", + "minutes-n": "{n} Min.", + "music-by-album": "Soundtracks nach Album", + "music-by-artist": "Soundtracks nach Künstler", "name": "Name", "new-version-available": "Neue Version verfügbar", "next-page": "Nächste Seite", @@ -95,10 +106,13 @@ "platform-created": "Plattform {name} erstellt", "platforms": "Plattformen", "platforms-n": "{n} Plattform | {n} Plattformen", + "play-all": "Alle abspielen", + "playlists": "Wiedergabelisten", "previous-page": "Vorherige Seite", "primary-navigation": "Hauptnavigation", "profile": "Profil", "random": "Zufällig", + "recently-added-soundtracks": "Kürzlich hinzugefügte Soundtracks", "remove": "Entfernen", "removing-from-filesystem": "Entfernen vom System", "required": "Erforderlich", @@ -116,11 +130,17 @@ "server-stats": "Serverstatistiken", "settings": "Einstellungen", "show-password": "Passwort anzeigen", + "shuffle": "Zufallswiedergabe", "size": "Größe", "size-on-disk": "Größe auf Festplatte", "slug": "Slug", "smart-collection": "Intelligente Sammlung", "smart-collections": "Intelligente Sammlungen", + "soundtrack-favorite-added": "Titel zu Favoriten hinzugefügt", + "soundtrack-favorite-failed": "Favoritenstatus konnte nicht geändert werden", + "soundtrack-favorite-removed": "Titel aus Favoriten entfernt", + "soundtracks-by-genre": "Soundtracks nach Genre", + "soundtracks-by-platform": "Soundtracks nach Plattform", "state": "Spielstand", "states": "Spielstände", "states-n": "{n} Spielstand | {n} Spielstände", diff --git a/frontend/src/locales/en_GB/common.json b/frontend/src/locales/en_GB/common.json index 16ef03a914..b3b4994bd9 100644 --- a/frontend/src/locales/en_GB/common.json +++ b/frontend/src/locales/en_GB/common.json @@ -8,11 +8,13 @@ "activity": "Activity", "add": "Add", "administration": "Administration", + "albums-n": "{n} album | {n} albums", "all": "All", "all-files-skipped": "All files skipped, nothing uploaded.", "all-platforms": "All platforms", "and": "and", "apply": "Apply", + "artists-n": "{n} artist | {n} artists", "ascii-only": "ASCII characters only", "back": "Back", "back-btn": "Back", @@ -42,6 +44,8 @@ "core": "Core", "create": "Create", "crt-mode": "CRT mode", + "decade-mix": "Decade Mix", + "decades-n": "{n} decade | {n} decades", "delete": "Delete", "details": "Details", "discard": "Discard", @@ -54,20 +58,24 @@ "edit": "Edit", "exclude-on-delete": "Exclude on delete", "expand": "Expand", + "favorite-soundtracks": "Favorite soundtracks", "filter": "Filter", "firmware": "Firmware", "first-page": "First page", "for": "for", + "free-radio": "Free Radio", "games": "Games", "games-header": "Games", "games-n": "{n} Game | {n} Games", "generate": "Generate", + "genres-n": "{n} genre | {n} genres", "got-it": "Got it", "hide-password": "Hide password", "home": "Home", "in-library": "In library", "invalid-email": "Invalid email", "invalid-name": "Invalid name", + "jukebox": "Jukebox", "kind": "Kind", "last-page": "Last page", "last-updated": "Last updated", @@ -79,6 +87,9 @@ "local": "Local", "logout": "Logout", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Soundtracks by Album", + "music-by-artist": "Soundtracks by Artist", "name": "Name", "new-version-available": "New version available", "next-page": "Next page", @@ -94,11 +105,14 @@ "platform": "Platform", "platform-created": "Platform {name} created", "platforms": "Platforms", - "platforms-n": "{n} Platform | {n} Platforms", + "platforms-n": "{n} platform | {n} platforms", + "play-all": "Play all", + "playlists": "Playlists", "previous-page": "Previous page", "primary-navigation": "Primary navigation", "profile": "Profile", "random": "Random", + "recently-added-soundtracks": "Recently added soundtracks", "remove": "Remove", "removing-from-filesystem": "Removing from filesystem", "required": "Required", @@ -116,11 +130,17 @@ "server-stats": "Server stats", "settings": "Settings", "show-password": "Show password", + "shuffle": "Shuffle", "size": "Size", "size-on-disk": "Size on disk", "slug": "Slug", "smart-collection": "Smart Collection", "smart-collections": "Smart Collections", + "soundtrack-favorite-added": "Track added to favorites", + "soundtrack-favorite-failed": "Failed to update track favorite", + "soundtrack-favorite-removed": "Track removed from favorites", + "soundtracks-by-genre": "Soundtracks by Genre", + "soundtracks-by-platform": "Soundtracks by Platform", "state": "State", "states": "States", "states-n": "{n} State | {n} States", diff --git a/frontend/src/locales/en_US/common.json b/frontend/src/locales/en_US/common.json index 16ef03a914..b3b4994bd9 100644 --- a/frontend/src/locales/en_US/common.json +++ b/frontend/src/locales/en_US/common.json @@ -8,11 +8,13 @@ "activity": "Activity", "add": "Add", "administration": "Administration", + "albums-n": "{n} album | {n} albums", "all": "All", "all-files-skipped": "All files skipped, nothing uploaded.", "all-platforms": "All platforms", "and": "and", "apply": "Apply", + "artists-n": "{n} artist | {n} artists", "ascii-only": "ASCII characters only", "back": "Back", "back-btn": "Back", @@ -42,6 +44,8 @@ "core": "Core", "create": "Create", "crt-mode": "CRT mode", + "decade-mix": "Decade Mix", + "decades-n": "{n} decade | {n} decades", "delete": "Delete", "details": "Details", "discard": "Discard", @@ -54,20 +58,24 @@ "edit": "Edit", "exclude-on-delete": "Exclude on delete", "expand": "Expand", + "favorite-soundtracks": "Favorite soundtracks", "filter": "Filter", "firmware": "Firmware", "first-page": "First page", "for": "for", + "free-radio": "Free Radio", "games": "Games", "games-header": "Games", "games-n": "{n} Game | {n} Games", "generate": "Generate", + "genres-n": "{n} genre | {n} genres", "got-it": "Got it", "hide-password": "Hide password", "home": "Home", "in-library": "In library", "invalid-email": "Invalid email", "invalid-name": "Invalid name", + "jukebox": "Jukebox", "kind": "Kind", "last-page": "Last page", "last-updated": "Last updated", @@ -79,6 +87,9 @@ "local": "Local", "logout": "Logout", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Soundtracks by Album", + "music-by-artist": "Soundtracks by Artist", "name": "Name", "new-version-available": "New version available", "next-page": "Next page", @@ -94,11 +105,14 @@ "platform": "Platform", "platform-created": "Platform {name} created", "platforms": "Platforms", - "platforms-n": "{n} Platform | {n} Platforms", + "platforms-n": "{n} platform | {n} platforms", + "play-all": "Play all", + "playlists": "Playlists", "previous-page": "Previous page", "primary-navigation": "Primary navigation", "profile": "Profile", "random": "Random", + "recently-added-soundtracks": "Recently added soundtracks", "remove": "Remove", "removing-from-filesystem": "Removing from filesystem", "required": "Required", @@ -116,11 +130,17 @@ "server-stats": "Server stats", "settings": "Settings", "show-password": "Show password", + "shuffle": "Shuffle", "size": "Size", "size-on-disk": "Size on disk", "slug": "Slug", "smart-collection": "Smart Collection", "smart-collections": "Smart Collections", + "soundtrack-favorite-added": "Track added to favorites", + "soundtrack-favorite-failed": "Failed to update track favorite", + "soundtrack-favorite-removed": "Track removed from favorites", + "soundtracks-by-genre": "Soundtracks by Genre", + "soundtracks-by-platform": "Soundtracks by Platform", "state": "State", "states": "States", "states-n": "{n} State | {n} States", diff --git a/frontend/src/locales/es_ES/common.json b/frontend/src/locales/es_ES/common.json index cf759e5081..ae3990c48e 100644 --- a/frontend/src/locales/es_ES/common.json +++ b/frontend/src/locales/es_ES/common.json @@ -8,11 +8,13 @@ "activity": "Actividad", "add": "Añadir", "administration": "Administración", + "albums-n": "{n} álbum | {n} álbumes", "all": "Todos", "all-files-skipped": "Se omitieron todos los archivos, nada que subir.", "all-platforms": "Todas las plataformas", "and": "y", "apply": "Aplicar", + "artists-n": "{n} artista | {n} artistas", "ascii-only": "Solo caracteres ASCII", "back": "Atrás", "back-btn": "Atrás", @@ -42,6 +44,8 @@ "core": "Núcleo", "create": "Crear", "crt-mode": "Modo CRT", + "decade-mix": "Mezcla por décadas", + "decades-n": "{n} década | {n} décadas", "delete": "Eliminar", "details": "Detalles", "discard": "Descartar", @@ -54,20 +58,24 @@ "edit": "Editar", "exclude-on-delete": "Excluir al eliminar", "expand": "Expandir", + "favorite-soundtracks": "Bandas sonoras favoritas", "filter": "Filtrar", "firmware": "Firmware", "first-page": "Primera página", "for": "para", + "free-radio": "Radio libre", "games": "Juegos", "games-header": "Juegos", "games-n": "{n} Juego | {n} Juegos", "generate": "Generar", + "genres-n": "{n} género | {n} géneros", "got-it": "Entendido", "hide-password": "Ocultar contraseña", "home": "Inicio", "in-library": "En biblioteca", "invalid-email": "Correo electrónico inválido", "invalid-name": "Nombre inválido", + "jukebox": "Rockola", "kind": "Tipo", "last-page": "Última página", "last-updated": "Última actualización", @@ -79,6 +87,9 @@ "local": "Local", "logout": "Cerrar sesión", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Bandas sonoras por álbum", + "music-by-artist": "Bandas sonoras por artista", "name": "Nombre", "new-version-available": "Nueva versión disponible", "next-page": "Siguiente", @@ -94,11 +105,14 @@ "platform": "Plataforma", "platform-created": "Plataforma creada", "platforms": "Plataformas", - "platforms-n": "{n} Plataforma | {n} Plataformas", + "platforms-n": "{n} plataforma | {n} plataformas", + "play-all": "Reproducir todo", + "playlists": "Listas de reproducción", "previous-page": "Anterior", "primary-navigation": "Navegación principal", "profile": "Perfil", "random": "Aleatorio", + "recently-added-soundtracks": "Bandas sonoras añadidas recientemente", "remove": "Eliminar", "removing-from-filesystem": "Eliminando del sistema", "required": "Requerido", @@ -116,11 +130,17 @@ "server-stats": "Estadísticas del servidor", "settings": "Ajustes", "show-password": "Mostrar contraseña", + "shuffle": "Mezclar", "size": "Tamaño", "size-on-disk": "Tamaño en disco", "slug": "Slug", "smart-collection": "Colección Inteligente", "smart-collections": "Colecciones Inteligentes", + "soundtrack-favorite-added": "Pista añadida a favoritos", + "soundtrack-favorite-failed": "No se pudo actualizar la pista favorita", + "soundtrack-favorite-removed": "Pista eliminada de favoritos", + "soundtracks-by-genre": "Bandas sonoras por género", + "soundtracks-by-platform": "Bandas sonoras por plataforma", "state": "Estado", "states": "Estados", "states-n": "{n} Estado | {n} Estados", diff --git a/frontend/src/locales/fr_FR/common.json b/frontend/src/locales/fr_FR/common.json index 859db3a0a2..4b32f91919 100644 --- a/frontend/src/locales/fr_FR/common.json +++ b/frontend/src/locales/fr_FR/common.json @@ -8,11 +8,13 @@ "activity": "Activité", "add": "Ajouter", "administration": "Administration", + "albums-n": "{n} album | {n} albums", "all": "Tout", "all-files-skipped": "Tous les fichiers ont été ignorés, rien n'a été téléversé.", "all-platforms": "Toutes les plateformes", "and": "et", "apply": "Appliquer", + "artists-n": "{n} artiste | {n} artistes", "ascii-only": "Uniquement des caractères ASCII", "back": "Retour", "back-btn": "Retour", @@ -42,6 +44,8 @@ "core": "Noyau", "create": "Créer", "crt-mode": "Mode CRT", + "decade-mix": "Mix par décennie", + "decades-n": "{n} décennie | {n} décennies", "delete": "Supprimer", "details": "Détails", "discard": "Abandonner", @@ -54,20 +58,24 @@ "edit": "Éditer", "exclude-on-delete": "Exclure lors de la suppression", "expand": "Développer", + "favorite-soundtracks": "Bandes-son favorites", "filter": "Filtrer", "firmware": "Micrologiciel", "first-page": "Première page", "for": "pour", + "free-radio": "Radio libre", "games": "Jeux", "games-header": "Jeux", "games-n": "{n} Jeu | {n} Jeux", "generate": "Générer", + "genres-n": "{n} genre | {n} genres", "got-it": "Compris", "hide-password": "Masquer le mot de passe", "home": "Accueil", "in-library": "Dans la bibliothèque", "invalid-email": "E-mail invalide", "invalid-name": "Nom invalide", + "jukebox": "Jukebox", "kind": "Type", "last-page": "Dernière page", "last-updated": "Dernière mise à jour", @@ -79,6 +87,9 @@ "local": "Local", "logout": "Se déconnecter", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Bandes-son par album", + "music-by-artist": "Bandes-son par artiste", "name": "Nom", "new-version-available": "Nouvelle version disponible", "next-page": "Page suivante", @@ -94,11 +105,14 @@ "platform": "Plateforme", "platform-created": "Plateforme {name} créée", "platforms": "Plateformes", - "platforms-n": "{n} Plateforme | {n} Plateformes", + "platforms-n": "{n} plateforme | {n} plateformes", + "play-all": "Tout lire", + "playlists": "Listes de lecture", "previous-page": "Page précédente", "primary-navigation": "Navigation principale", "profile": "Profil", "random": "Aléatoire", + "recently-added-soundtracks": "Bandes-son ajoutées récemment", "remove": "Retirer", "removing-from-filesystem": "Suppression du système", "required": "Requis", @@ -116,11 +130,17 @@ "server-stats": "Statistiques du serveur", "settings": "Paramètres", "show-password": "Afficher le mot de passe", + "shuffle": "Lecture aléatoire", "size": "Taille", "size-on-disk": "Taille sur le disque", "slug": "Slug", "smart-collection": "Collection Intelligente", "smart-collections": "Collections Intelligentes", + "soundtrack-favorite-added": "Piste ajoutée aux favoris", + "soundtrack-favorite-failed": "Impossible de modifier la piste favorite", + "soundtrack-favorite-removed": "Piste retirée des favoris", + "soundtracks-by-genre": "Bandes-son par genre", + "soundtracks-by-platform": "Bandes-son par plateforme", "state": "État", "states": "États", "states-n": "{n} État | {n} États", diff --git a/frontend/src/locales/hu_HU/common.json b/frontend/src/locales/hu_HU/common.json index ca7553857f..bfc7a5ee5a 100644 --- a/frontend/src/locales/hu_HU/common.json +++ b/frontend/src/locales/hu_HU/common.json @@ -8,11 +8,13 @@ "activity": "Tevékenység", "add": "Hozzáadás", "administration": "Adminisztráció", + "albums-n": "{n} album | {n} album", "all": "Összes", "all-files-skipped": "Minden fájl kihagyva, nincs feltöltött elem.", "all-platforms": "Összes platform", "and": "és", "apply": "Alkalmaz", + "artists-n": "{n} előadó | {n} előadó", "ascii-only": "Csak ASCII karakterek", "back": "Vissza", "back-btn": "Vissza", @@ -42,6 +44,8 @@ "core": "Mag", "create": "Létrehoz", "crt-mode": "CRT mode", + "decade-mix": "Évtizedmix", + "decades-n": "{n} évtized | {n} évtized", "delete": "Törlés", "details": "Részletek", "discard": "Elvetés", @@ -54,20 +58,24 @@ "edit": "Szerkesztés", "exclude-on-delete": "Elem kihagyása törléskor", "expand": "Kibontás", + "favorite-soundtracks": "Kedvenc filmzenék", "filter": "Szűrő", "firmware": "Firmware", "first-page": "Első oldal", "for": "ehhez:", + "free-radio": "Szabad rádió", "games": "Játékok", "games-header": "Játékok", "games-n": "{n} Játék | {n} Játékok", "generate": "Generálás", + "genres-n": "{n} műfaj | {n} műfaj", "got-it": "Értem", "hide-password": "Jelszó elrejtése", "home": "Kezdőlap", "in-library": "A könyvtárban", "invalid-email": "Érvénytelen e-mail", "invalid-name": "Érvénytelen név", + "jukebox": "Zenelejátszó", "kind": "Típus", "last-page": "Utolsó oldal", "last-updated": "Utoljára frissítve", @@ -79,6 +87,9 @@ "local": "Helyi", "logout": "Kijelentkezés", "logs": "Logs", + "minutes-n": "{n} perc", + "music-by-album": "Filmzenék album szerint", + "music-by-artist": "Filmzenék előadó szerint", "name": "Név", "new-version-available": "Új verzió érhető el", "next-page": "Következő oldal", @@ -94,11 +105,14 @@ "platform": "Platform", "platform-created": "{name} platform létrehozva", "platforms": "Platformok", - "platforms-n": "{n} Platform | {n} Platformok", + "platforms-n": "{n} platform | {n} platform", + "play-all": "Összes lejátszása", + "playlists": "Lejátszási listák", "previous-page": "Előző oldal", "primary-navigation": "Elsődleges navigáció", "profile": "Profil", "random": "Véletlenszerű", + "recently-added-soundtracks": "Nemrég hozzáadott filmzenék", "remove": "Eltávolítás", "removing-from-filesystem": "Eltávolítás a fájlrendszerből", "required": "Szükséges", @@ -116,11 +130,17 @@ "server-stats": "Szerver statisztika", "settings": "Beállítások", "show-password": "Jelszó megjelenítése", + "shuffle": "Véletlen sorrend", "size": "Méret", "size-on-disk": "Méret a lemezen", "slug": "Slug", "smart-collection": "Intelligens gyűjtemény", "smart-collections": "Intelligens gyűjtemények", + "soundtrack-favorite-added": "Szám hozzáadva a kedvencekhez", + "soundtrack-favorite-failed": "Nem sikerült módosítani a kedvenc számot", + "soundtrack-favorite-removed": "Szám eltávolítva a kedvencek közül", + "soundtracks-by-genre": "Filmzenék műfaj szerint", + "soundtracks-by-platform": "Filmzenék platform szerint", "state": "Állás", "states": "Állások", "states-n": "{n} Állás | {n} Állások", diff --git a/frontend/src/locales/it_IT/common.json b/frontend/src/locales/it_IT/common.json index 94d5e3d60f..1648fc2fe2 100644 --- a/frontend/src/locales/it_IT/common.json +++ b/frontend/src/locales/it_IT/common.json @@ -8,11 +8,13 @@ "activity": "Attività", "add": "Aggiungi", "administration": "Amministrazione", + "albums-n": "{n} album | {n} album", "all": "Tutti", "all-files-skipped": "Tutti i file saltati, niente è stato caricato.", "all-platforms": "Tutte le piattaforme", "and": "e", "apply": "Applica", + "artists-n": "{n} artista | {n} artisti", "ascii-only": "Solo caratteri ASCII", "back": "Indietro", "back-btn": "Indietro", @@ -42,6 +44,8 @@ "core": "Core", "create": "Crea", "crt-mode": "Modalità CRT", + "decade-mix": "Mix per decennio", + "decades-n": "{n} decennio | {n} decenni", "delete": "Elimina", "details": "Dettagli", "discard": "Scarta", @@ -54,20 +58,24 @@ "edit": "Modifica", "exclude-on-delete": "Escludi al momento dell'eliminazione", "expand": "Espandi", + "favorite-soundtracks": "Colonne sonore preferite", "filter": "Filtro", "firmware": "Firmware", "first-page": "Prima pagina", "for": "per", + "free-radio": "Radio libera", "games": "Giochi", "games-header": "Giochi", "games-n": "{n} Gioco | {n} Giochi", "generate": "Genera", + "genres-n": "{n} genere | {n} generi", "got-it": "Ho capito", "hide-password": "Nascondi password", "home": "Home", "in-library": "In libreria", "invalid-email": "Email non valida", "invalid-name": "Nome non valido", + "jukebox": "Jukebox", "kind": "Tipo", "last-page": "Ultima pagina", "last-updated": "Ultimo aggiornamento", @@ -79,6 +87,9 @@ "local": "Locale", "logout": "Logout", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Colonne sonore per album", + "music-by-artist": "Colonne sonore per artista", "name": "Nome", "new-version-available": "Nuova versione disponibile", "next-page": "Pagina successiva", @@ -94,11 +105,14 @@ "platform": "Piattaforma", "platform-created": "Piattaforma {name} creata", "platforms": "Piattaforme", - "platforms-n": "{n} Piattaforma | {n} Piattaforme", + "platforms-n": "{n} piattaforma | {n} piattaforme", + "play-all": "Riproduci tutto", + "playlists": "Playlist", "previous-page": "Pagina precedente", "primary-navigation": "Navigazione principale", "profile": "Profilo", "random": "Casuale", + "recently-added-soundtracks": "Colonne sonore aggiunte di recente", "remove": "Rimuovi", "removing-from-filesystem": "Rimozione dal sistema", "required": "Obbligatorio", @@ -116,11 +130,17 @@ "server-stats": "Statistiche del server", "settings": "Impostazioni", "show-password": "Mostra password", + "shuffle": "Riproduzione casuale", "size": "Dimensione", "size-on-disk": "Dimensione sul disco", "slug": "Slug", "smart-collection": "Collezione Intelligente", "smart-collections": "Collezioni Intelligenti", + "soundtrack-favorite-added": "Brano aggiunto ai preferiti", + "soundtrack-favorite-failed": "Impossibile aggiornare il brano preferito", + "soundtrack-favorite-removed": "Brano rimosso dai preferiti", + "soundtracks-by-genre": "Colonne sonore per genere", + "soundtracks-by-platform": "Colonne sonore per piattaforma", "state": "Stato", "states": "Stati", "states-n": "{n} Stato | {n} Stati", diff --git a/frontend/src/locales/ja_JP/common.json b/frontend/src/locales/ja_JP/common.json index b5eafba1fb..ceb59bb412 100644 --- a/frontend/src/locales/ja_JP/common.json +++ b/frontend/src/locales/ja_JP/common.json @@ -8,11 +8,13 @@ "activity": "アクティビティ", "add": "追加", "administration": "管理者メニュー", + "albums-n": "{n}件のアルバム", "all": "すべて", "all-files-skipped": "すべてのファイルがスキップされ、アップロードされたものはありません。", "all-platforms": "すべてのプラットフォーム", "and": "と", "apply": "適用", + "artists-n": "{n}人のアーティスト", "ascii-only": "ASCII文字のみ", "back": "戻る", "back-btn": "戻る", @@ -42,6 +44,8 @@ "core": "コア", "create": "作成", "crt-mode": "CRT mode", + "decade-mix": "年代別ミックス", + "decades-n": "{n}年代", "delete": "削除", "details": "詳細", "discard": "破棄", @@ -54,20 +58,24 @@ "edit": "編集", "exclude-on-delete": "削除時に除外", "expand": "展開", + "favorite-soundtracks": "お気に入りのサウンドトラック", "filter": "フィルタ", "firmware": "ファームウェア", "first-page": "最初のページ", "for": "対象:", + "free-radio": "フリーラジオ", "games": "ゲーム", "games-header": "ゲーム", "games-n": "{n} ゲーム | {n} ゲーム", "generate": "生成", + "genres-n": "{n}件のジャンル", "got-it": "了解", "hide-password": "パスワードを隠す", "home": "ホーム", "in-library": "ライブラリ内", "invalid-email": "無効なメールアドレス", "invalid-name": "無効な名前", + "jukebox": "ジュークボックス", "kind": "種類", "last-page": "最後のページ", "last-updated": "最終更新", @@ -79,6 +87,9 @@ "local": "ローカル", "logout": "ログアウト", "logs": "Logs", + "minutes-n": "{n}分", + "music-by-album": "アルバム別サウンドトラック", + "music-by-artist": "アーティスト別サウンドトラック", "name": "名前", "new-version-available": "新しいバージョンが利用可能です", "next-page": "次のページ", @@ -94,11 +105,14 @@ "platform": "プラットフォーム", "platform-created": "プラットフォーム{name}を作成しました", "platforms": "プラットフォーム", - "platforms-n": "{n} プラットフォーム | {n} プラットフォームs", + "platforms-n": "{n}件のプラットフォーム", + "play-all": "すべて再生", + "playlists": "プレイリスト", "previous-page": "前のページ", "primary-navigation": "メインナビゲーション", "profile": "プロファイル", "random": "ランダム", + "recently-added-soundtracks": "最近追加されたサウンドトラック", "remove": "削除", "removing-from-filesystem": "システムから削除中", "required": "必須", @@ -116,11 +130,17 @@ "server-stats": "サーバーステータス", "settings": "設定", "show-password": "パスワードを表示", + "shuffle": "シャッフル", "size": "サイズ", "size-on-disk": "ディスク上のサイズ", "slug": "スラッグ", "smart-collection": "スマートコレクション", "smart-collections": "スマートコレクション", + "soundtrack-favorite-added": "トラックをお気に入りに追加しました", + "soundtrack-favorite-failed": "トラックのお気に入りを更新できませんでした", + "soundtrack-favorite-removed": "トラックをお気に入りから削除しました", + "soundtracks-by-genre": "ジャンル別サウンドトラック", + "soundtracks-by-platform": "プラットフォーム別サウンドトラック", "state": "ステートセーブ", "states": "ステートセーブ", "states-n": "{n} ステートセーブ | {n} ステートセーブ", diff --git a/frontend/src/locales/ko_KR/common.json b/frontend/src/locales/ko_KR/common.json index 6bcaf0d7ed..40b6358f57 100644 --- a/frontend/src/locales/ko_KR/common.json +++ b/frontend/src/locales/ko_KR/common.json @@ -8,11 +8,13 @@ "activity": "활동", "add": "추가", "administration": "유저 관리", + "albums-n": "앨범 {n}개", "all": "전체", "all-files-skipped": "모든 파일이 건너뛰어졌습니다. 업로드된 파일이 없습니다.", "all-platforms": "모든 플랫폼", "and": "및", "apply": "적용", + "artists-n": "아티스트 {n}명", "ascii-only": "ASCII 문자만", "back": "뒤로", "back-btn": "뒤로", @@ -42,6 +44,8 @@ "core": "코어", "create": "생성", "crt-mode": "CRT mode", + "decade-mix": "연대별 믹스", + "decades-n": "{n}개 연대", "delete": "삭제", "details": "자세히", "discard": "취소", @@ -54,20 +58,24 @@ "edit": "수정", "exclude-on-delete": "삭제 시 제외", "expand": "펼치기", + "favorite-soundtracks": "즐겨찾는 사운드트랙", "filter": "필터", "firmware": "펌웨어", "first-page": "첫 페이지", "for": "대상", + "free-radio": "자유 라디오", "games": "게임", "games-header": "게임", "games-n": "게임 {n}개 | 게임 {n}개", "generate": "생성", + "genres-n": "장르 {n}개", "got-it": "확인", "hide-password": "비밀번호 숨기기", "home": "홈", "in-library": "라이브러리에 있음", "invalid-email": "유효하지 않은 이메일", "invalid-name": "유효하지 않은 이름", + "jukebox": "주크박스", "kind": "종류", "last-page": "마지막 페이지", "last-updated": "마지막 업데이트", @@ -79,6 +87,9 @@ "local": "로컬", "logout": "로그아웃", "logs": "Logs", + "minutes-n": "{n}분", + "music-by-album": "앨범별 사운드트랙", + "music-by-artist": "아티스트별 사운드트랙", "name": "이름", "new-version-available": "새 버전 사용 가능", "next-page": "다음 페이지", @@ -94,11 +105,14 @@ "platform": "플랫폼", "platform-created": "플랫폼 {name}이(가) 생성되었습니다", "platforms": "플랫폼", - "platforms-n": "{n}가지 플랫폼 | {n}가지 플랫폼", + "platforms-n": "플랫폼 {n}개", + "play-all": "모두 재생", + "playlists": "재생목록", "previous-page": "이전 페이지", "primary-navigation": "기본 탐색", "profile": "프로필", "random": "무작위", + "recently-added-soundtracks": "최근 추가된 사운드트랙", "remove": "제거", "removing-from-filesystem": "시스템에서 제거 중", "required": "필수", @@ -116,11 +130,17 @@ "server-stats": "서버 통계", "settings": "설정", "show-password": "비밀번호 표시", + "shuffle": "셔플", "size": "크기", "size-on-disk": "디스크 크기", "slug": "슬러그", "smart-collection": "스마트 모음집", "smart-collections": "스마트 모음집", + "soundtrack-favorite-added": "트랙을 즐겨찾기에 추가했습니다", + "soundtrack-favorite-failed": "트랙 즐겨찾기를 변경하지 못했습니다", + "soundtrack-favorite-removed": "트랙을 즐겨찾기에서 삭제했습니다", + "soundtracks-by-genre": "장르별 사운드트랙", + "soundtracks-by-platform": "플랫폼별 사운드트랙", "state": "저장 상태", "states": "저장 상태", "states-n": "저장 상태 {n}개 | 저장 상태 {n}개", diff --git a/frontend/src/locales/pl_PL/common.json b/frontend/src/locales/pl_PL/common.json index 4853afe891..e738b70051 100644 --- a/frontend/src/locales/pl_PL/common.json +++ b/frontend/src/locales/pl_PL/common.json @@ -8,11 +8,13 @@ "activity": "Aktywność", "add": "Dodaj", "administration": "Administracja", + "albums-n": "{n} album | {n} albumy | {n} albumów", "all": "Wszystkie", "all-files-skipped": "Wszystkie pliki pominięte, nic nie zostało przesłane.", "all-platforms": "Wszystkie platformy", "and": "i", "apply": "Zastosuj", + "artists-n": "{n} wykonawca | {n} wykonawców", "ascii-only": "Tylko znaki ASCII", "back": "Wstecz", "back-btn": "Wstecz", @@ -42,6 +44,8 @@ "core": "Rdzeń", "create": "Utwórz", "crt-mode": "CRT mode", + "decade-mix": "Miks dekad", + "decades-n": "{n} dekada | {n} dekady | {n} dekad", "delete": "Usuń", "details": "Szczegóły", "discard": "Odrzuć", @@ -54,20 +58,24 @@ "edit": "Edytuj", "exclude-on-delete": "Wyklucz przy usunięciu", "expand": "Rozwiń", + "favorite-soundtracks": "Ulubione ścieżki dźwiękowe", "filter": "Filtruj", "firmware": "Oprogramowanie", "first-page": "Pierwsza strona", "for": "dla", + "free-radio": "Wolne radio", "games": "Gry", "games-header": "Gry", "games-n": "{n} Gra | {n} Gry | {n} Gier", "generate": "Generuj", + "genres-n": "{n} gatunek | {n} gatunki | {n} gatunków", "got-it": "Rozumiem", "hide-password": "Ukryj hasło", "home": "Strona główna", "in-library": "W bibliotece", "invalid-email": "Nieprawidłowy email", "invalid-name": "Nieprawidłowa nazwa", + "jukebox": "Szafa grająca", "kind": "Rodzaj", "last-page": "Ostatnia strona", "last-updated": "Ostatnio zaktualizowane", @@ -79,6 +87,9 @@ "local": "Lokalne", "logout": "Wyloguj się", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Ścieżki dźwiękowe według albumu", + "music-by-artist": "Ścieżki dźwiękowe według wykonawcy", "name": "Nazwa", "new-version-available": "Dostępna jest nowa wersja", "next-page": "Następna strona", @@ -94,11 +105,14 @@ "platform": "Platforma", "platform-created": "Platforma {name} utworzona", "platforms": "Platformy", - "platforms-n": "{n} Platforma | {n} Platformy | {n} Platform", + "platforms-n": "{n} platforma | {n} platformy | {n} platform", + "play-all": "Odtwórz wszystko", + "playlists": "Playlisty", "previous-page": "Poprzednia strona", "primary-navigation": "Nawigacja główna", "profile": "Profil", "random": "Losowo", + "recently-added-soundtracks": "Ostatnio dodane ścieżki dźwiękowe", "remove": "Usuń", "removing-from-filesystem": "Usuwanie z systemu", "required": "Wymagane", @@ -116,11 +130,17 @@ "server-stats": "Statystyki serwera", "settings": "Ustawienia", "show-password": "Pokaż hasło", + "shuffle": "Odtwarzanie losowe", "size": "Rozmiar", "size-on-disk": "Rozmiar na dysku", "slug": "Identyfikator (slug)", "smart-collection": "Inteligentna kolekcja", "smart-collections": "Inteligentne kolekcje", + "soundtrack-favorite-added": "Utwór dodano do ulubionych", + "soundtrack-favorite-failed": "Nie udało się zmienić ulubionego utworu", + "soundtrack-favorite-removed": "Utwór usunięto z ulubionych", + "soundtracks-by-genre": "Ścieżki dźwiękowe według gatunku", + "soundtracks-by-platform": "Ścieżki dźwiękowe według platformy", "state": "Stan", "states": "Stany", "states-n": "{n} Stan | {n} Stany | {n} Stanów", diff --git a/frontend/src/locales/pt_BR/common.json b/frontend/src/locales/pt_BR/common.json index 82e5325113..cd9eadc91e 100644 --- a/frontend/src/locales/pt_BR/common.json +++ b/frontend/src/locales/pt_BR/common.json @@ -8,11 +8,13 @@ "activity": "Atividade", "add": "Adicionar", "administration": "Administração", + "albums-n": "{n} álbum | {n} álbuns", "all": "Todos", "all-files-skipped": "Todos os arquivos foram ignorados, nada foi enviado.", "all-platforms": "Todas as plataformas", "and": "e", "apply": "Aplicar", + "artists-n": "{n} artista | {n} artistas", "ascii-only": "Somente caracteres ASCII", "back": "Voltar", "back-btn": "Voltar", @@ -42,6 +44,8 @@ "core": "Núcleo", "create": "Criar", "crt-mode": "Modo CRT", + "decade-mix": "Mix por década", + "decades-n": "{n} década | {n} décadas", "delete": "Excluir", "details": "Detalhes", "discard": "Descartar", @@ -54,20 +58,24 @@ "edit": "Editar", "exclude-on-delete": "Excluir ao excluir", "expand": "Expandir", + "favorite-soundtracks": "Trilhas sonoras favoritas", "filter": "Filtrar", "firmware": "Firmware", "first-page": "Primeira página", "for": "para", + "free-radio": "Rádio livre", "games": "Jogos", "games-header": "Jogos", "games-n": "{n} Jogo | {n} Jogos", "generate": "Gerar", + "genres-n": "{n} gênero | {n} gêneros", "got-it": "Entendi", "hide-password": "Ocultar senha", "home": "Início", "in-library": "Na biblioteca", "invalid-email": "Email inválido", "invalid-name": "Nome inválido", + "jukebox": "Jukebox", "kind": "Tipo", "last-page": "Última página", "last-updated": "Última atualização", @@ -79,6 +87,9 @@ "local": "Local", "logout": "Sair", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Trilhas sonoras por álbum", + "music-by-artist": "Trilhas sonoras por artista", "name": "Nome", "new-version-available": "Nova versão disponível", "next-page": "Próxima página", @@ -94,11 +105,14 @@ "platform": "Plataforma", "platform-created": "Plataforma {name} criada", "platforms": "Plataformas", - "platforms-n": "{n} Plataforma | {n} Plataformas", + "platforms-n": "{n} plataforma | {n} plataformas", + "play-all": "Reproduzir tudo", + "playlists": "Listas de reprodução", "previous-page": "Página anterior", "primary-navigation": "Navegação principal", "profile": "Perfil", "random": "Aleatório", + "recently-added-soundtracks": "Trilhas sonoras adicionadas recentemente", "remove": "Remover", "removing-from-filesystem": "Removendo do sistema", "required": "Requerido", @@ -116,11 +130,17 @@ "server-stats": "Estatísticas do servidor", "settings": "Configurações", "show-password": "Mostrar senha", + "shuffle": "Embaralhar", "size": "Tamanho", "size-on-disk": "Tamanho no disco", "slug": "Slug", "smart-collection": "Coleção Inteligente", "smart-collections": "Coleções Inteligentes", + "soundtrack-favorite-added": "Faixa adicionada aos favoritos", + "soundtrack-favorite-failed": "Falha ao atualizar a faixa favorita", + "soundtrack-favorite-removed": "Faixa removida dos favoritos", + "soundtracks-by-genre": "Trilhas sonoras por gênero", + "soundtracks-by-platform": "Trilhas sonoras por plataforma", "state": "Estado", "states": "Estados", "states-n": "{n} Estado | {n} Estados", diff --git a/frontend/src/locales/ro_RO/common.json b/frontend/src/locales/ro_RO/common.json index fec36b7e18..6471367d9a 100644 --- a/frontend/src/locales/ro_RO/common.json +++ b/frontend/src/locales/ro_RO/common.json @@ -8,11 +8,13 @@ "activity": "Activitate", "add": "Adaugă", "administration": "Administrare", + "albums-n": "{n} album | {n} albume", "all": "Toate", "all-files-skipped": "Toate fișierele au fost ignorate, nimic nu a fost încărcat.", "all-platforms": "Toate platformele", "and": "și", "apply": "Aplică", + "artists-n": "{n} artist | {n} artiști", "ascii-only": "Caractere ASCII numai", "back": "Înapoi", "back-btn": "Înapoi", @@ -42,6 +44,8 @@ "core": "Nucleu", "create": "Creează", "crt-mode": "CRT mode", + "decade-mix": "Mix pe decenii", + "decades-n": "{n} deceniu | {n} decenii", "delete": "Șterge", "details": "Detalii", "discard": "Renunță", @@ -54,20 +58,24 @@ "edit": "Editează", "exclude-on-delete": "Excluzând la ștergere", "expand": "Extinde", + "favorite-soundtracks": "Coloane sonore favorite", "filter": "Filtru", "firmware": "Firmware", "first-page": "Prima pagină", "for": "pentru", + "free-radio": "Radio liber", "games": "Jocuri", "games-header": "Jocuri", "games-n": "{n} Joc | {n} Jocuri", "generate": "Generează", + "genres-n": "{n} gen | {n} genuri", "got-it": "Am înțeles", "hide-password": "Ascunde parola", "home": "Acasă", "in-library": "În bibliotecă", "invalid-email": "Email invalid", "invalid-name": "Nume invalid", + "jukebox": "Tonomat", "kind": "Tip", "last-page": "Ultima pagină", "last-updated": "Ultima actualizare", @@ -79,6 +87,9 @@ "local": "Local", "logout": "Deconectare", "logs": "Logs", + "minutes-n": "{n} min", + "music-by-album": "Coloane sonore după album", + "music-by-artist": "Coloane sonore după artist", "name": "Nume", "new-version-available": "Versiune nouă disponibilă", "next-page": "Pagina următoare", @@ -94,11 +105,14 @@ "platform": "Platformă", "platform-created": "Platforma {name} a fost creată", "platforms": "Platforme", - "platforms-n": "{n} Platformă | {n} Platforme", + "platforms-n": "{n} platformă | {n} platforme", + "play-all": "Redă tot", + "playlists": "Liste de redare", "previous-page": "Pagina anterioară", "primary-navigation": "Navigare principală", "profile": "Profil", "random": "Aleatoriu", + "recently-added-soundtracks": "Coloane sonore adăugate recent", "remove": "Elimină", "removing-from-filesystem": "Ștergere din sistem", "required": "Obligatoriu", @@ -116,11 +130,17 @@ "server-stats": "Statistici server", "settings": "Setări", "show-password": "Afișează parola", + "shuffle": "Amestecă", "size": "Dimensiune", "size-on-disk": "Dimensiune pe disc", "slug": "Slug", "smart-collection": "Colecție Inteligentă", "smart-collections": "Colecții Inteligente", + "soundtrack-favorite-added": "Piesa a fost adăugată la favorite", + "soundtrack-favorite-failed": "Nu s-a putut actualiza piesa favorită", + "soundtrack-favorite-removed": "Piesa a fost eliminată din favorite", + "soundtracks-by-genre": "Coloane sonore după gen", + "soundtracks-by-platform": "Coloane sonore după platformă", "state": "Stare", "states": "Stări", "states-n": "{n} Stare | {n} Stări", diff --git a/frontend/src/locales/ru_RU/common.json b/frontend/src/locales/ru_RU/common.json index 5cecac00c0..aac1f5b301 100644 --- a/frontend/src/locales/ru_RU/common.json +++ b/frontend/src/locales/ru_RU/common.json @@ -8,11 +8,13 @@ "activity": "Активность", "add": "Добавить", "administration": "Администрирование", + "albums-n": "{n} альбом | {n} альбома | {n} альбомов", "all": "Все", "all-files-skipped": "Все файлы пропущены, ничего не загружено.", "all-platforms": "Все платформы", "and": "и", "apply": "Применить", + "artists-n": "{n} исполнитель | {n} исполнителя | {n} исполнителей", "ascii-only": "Только ASCII символы", "back": "Назад", "back-btn": "Назад", @@ -42,6 +44,8 @@ "core": "Ядро", "create": "Создать", "crt-mode": "CRT mode", + "decade-mix": "Микс по десятилетиям", + "decades-n": "{n} десятилетие | {n} десятилетия | {n} десятилетий", "delete": "Удалить", "details": "Подробности", "discard": "Отменить", @@ -54,20 +58,24 @@ "edit": "Редактировать", "exclude-on-delete": "Исключить при удалении", "expand": "Развернуть", + "favorite-soundtracks": "Избранные саундтреки", "filter": "Фильтр", "firmware": "Прошивка", "first-page": "Первая страница", "for": "для", + "free-radio": "Свободное радио", "games": "Игры", "games-header": "Игры", "games-n": "{n} Игра | {n} Игры", "generate": "Сгенерировать", + "genres-n": "{n} жанр | {n} жанра | {n} жанров", "got-it": "Понятно", "hide-password": "Скрыть пароль", "home": "Главная", "in-library": "В библиотеке", "invalid-email": "Недопустимый адрес электронной почты", "invalid-name": "Недопустимое имя", + "jukebox": "Музыкальный автомат", "kind": "Тип", "last-page": "Последняя страница", "last-updated": "Последнее обновление", @@ -79,6 +87,9 @@ "local": "Локально", "logout": "Выйти", "logs": "Logs", + "minutes-n": "{n} мин", + "music-by-album": "Саундтреки по альбомам", + "music-by-artist": "Саундтреки по исполнителям", "name": "Имя", "new-version-available": "Доступна новая версия", "next-page": "Следующая страница", @@ -94,11 +105,14 @@ "platform": "Платформа", "platform-created": "Платформа {name} создана", "platforms": "Платформы", - "platforms-n": "{n} Платформа | {n} Платформы", + "platforms-n": "{n} платформа | {n} платформы | {n} платформ", + "play-all": "Воспроизвести всё", + "playlists": "Плейлисты", "previous-page": "Предыдущая страница", "primary-navigation": "Основная навигация", "profile": "Профиль", "random": "Случайный", + "recently-added-soundtracks": "Недавно добавленные саундтреки", "remove": "Удалить", "removing-from-filesystem": "Удаление из системы", "required": "Обязательное поле", @@ -116,11 +130,17 @@ "server-stats": "Статистика сервера", "settings": "Настройки", "show-password": "Показать пароль", + "shuffle": "Перемешать", "size": "Размер", "size-on-disk": "Размер на диске", "slug": "Слаг", "smart-collection": "Умная коллекция", "smart-collections": "Умные коллекции", + "soundtrack-favorite-added": "Трек добавлен в избранное", + "soundtrack-favorite-failed": "Не удалось изменить избранный трек", + "soundtrack-favorite-removed": "Трек удалён из избранного", + "soundtracks-by-genre": "Саундтреки по жанрам", + "soundtracks-by-platform": "Саундтреки по платформам", "state": "Состояние", "states": "Состояния", "states-n": "{n} Состояние | {n} Состояния", diff --git a/frontend/src/locales/tr_TR/common.json b/frontend/src/locales/tr_TR/common.json index 61281d6309..a5c1046188 100644 --- a/frontend/src/locales/tr_TR/common.json +++ b/frontend/src/locales/tr_TR/common.json @@ -8,11 +8,13 @@ "activity": "Aktivite", "add": "Ekle", "administration": "Yönetim", + "albums-n": "{n} albüm", "all": "Tümü", "all-files-skipped": "Tüm dosyalar atlandı, hiçbir şey yüklenmedi.", "all-platforms": "Tüm platformlar", "and": "ve", "apply": "Uygula", + "artists-n": "{n} sanatçı", "ascii-only": "Yalnızca ASCII karakterler", "back": "Geri", "back-btn": "Geri", @@ -42,6 +44,8 @@ "core": "Çekirdek", "create": "Oluştur", "crt-mode": "CRT modu", + "decade-mix": "On yıl karması", + "decades-n": "{n} on yıl", "delete": "Sil", "details": "Ayrıntılar", "discard": "Vazgeç", @@ -54,20 +58,24 @@ "edit": "Düzenle", "exclude-on-delete": "Silerken hariç tut", "expand": "Genişlet", + "favorite-soundtracks": "Favori oyun müzikleri", "filter": "Filtrele", "firmware": "Yazılım (Firmware)", "first-page": "İlk sayfa", "for": "için", + "free-radio": "Serbest radyo", "games": "Oyunlar", "games-header": "Oyunlar", "games-n": "{n} Oyun | {n} Oyun", "generate": "Oluştur", + "genres-n": "{n} tür", "got-it": "Anladım", "hide-password": "Şifreyi gizle", "home": "Ana sayfa", "in-library": "Kütüphanede", "invalid-email": "Geçersiz e-posta", "invalid-name": "Geçersiz isim", + "jukebox": "Müzik kutusu", "kind": "Tür", "last-page": "Son sayfa", "last-updated": "Son güncelleme", @@ -79,6 +87,9 @@ "local": "Yerel", "logout": "Çıkış yap", "logs": "Günlükler", + "minutes-n": "{n} dk", + "music-by-album": "Albüme göre oyun müzikleri", + "music-by-artist": "Sanatçıya göre oyun müzikleri", "name": "İsim", "new-version-available": "Yeni sürüm mevcut", "next-page": "Sonraki sayfa", @@ -94,11 +105,14 @@ "platform": "Platform", "platform-created": "{name} platformu oluşturuldu", "platforms": "Platformlar", - "platforms-n": "{n} Platform | {n} Platform", + "platforms-n": "{n} platform", + "play-all": "Tümünü oynat", + "playlists": "Çalma listeleri", "previous-page": "Önceki sayfa", "primary-navigation": "Ana gezinme", "profile": "Profil", "random": "Rastgele", + "recently-added-soundtracks": "Son eklenen oyun müzikleri", "remove": "Kaldır", "removing-from-filesystem": "Dosya sisteminden kaldırılıyor", "required": "Zorunlu", @@ -116,11 +130,17 @@ "server-stats": "Sunucu istatistikleri", "settings": "Ayarlar", "show-password": "Şifreyi göster", + "shuffle": "Karıştır", "size": "Boyut", "size-on-disk": "Diskteki boyut", "slug": "Slug", "smart-collection": "Akıllı Koleksiyon", "smart-collections": "Akıllı Koleksiyonlar", + "soundtrack-favorite-added": "Parça favorilere eklendi", + "soundtrack-favorite-failed": "Favori parça güncellenemedi", + "soundtrack-favorite-removed": "Parça favorilerden kaldırıldı", + "soundtracks-by-genre": "Türe göre oyun müzikleri", + "soundtracks-by-platform": "Platforma göre oyun müzikleri", "state": "Durum kaydı", "states": "Durum kayıtları", "states-n": "{n} Durum kaydı | {n} Durum kaydı", diff --git a/frontend/src/locales/zh_CN/common.json b/frontend/src/locales/zh_CN/common.json index 0a4d803b85..7ae451d82c 100644 --- a/frontend/src/locales/zh_CN/common.json +++ b/frontend/src/locales/zh_CN/common.json @@ -8,11 +8,13 @@ "activity": "动态", "add": "添加", "administration": "管理", + "albums-n": "{n} 张专辑", "all": "全部", "all-files-skipped": "所有文件均已跳过,未上传任何内容。", "all-platforms": "所有平台", "and": "和", "apply": "应用", + "artists-n": "{n} 位艺术家", "ascii-only": "仅限ASCII字符", "back": "返回", "back-btn": "返回", @@ -42,6 +44,8 @@ "core": "核心", "create": "创建", "crt-mode": "CRT mode", + "decade-mix": "年代混音", + "decades-n": "{n} 个年代", "delete": "删除", "details": "详情", "discard": "丢弃", @@ -54,20 +58,24 @@ "edit": "编辑", "exclude-on-delete": "删除时排除", "expand": "展开", + "favorite-soundtracks": "收藏的原声音乐", "filter": "筛选", "firmware": "固件", "first-page": "第一页", "for": "用于", + "free-radio": "自由电台", "games": "游戏", "games-header": "游戏", "games-n": "{n} 游戏 | {n} 游戏", "generate": "生成", + "genres-n": "{n} 个类型", "got-it": "知道了", "hide-password": "隐藏密码", "home": "首页", "in-library": "在游戏库中", "invalid-email": "无效的电子邮件", "invalid-name": "无效的名称", + "jukebox": "音乐盒", "kind": "种类", "last-page": "最后一页", "last-updated": "最后更新", @@ -79,6 +87,9 @@ "local": "本地", "logout": "注销", "logs": "Logs", + "minutes-n": "{n} 分钟", + "music-by-album": "按专辑浏览原声音乐", + "music-by-artist": "按艺术家浏览原声音乐", "name": "名称", "new-version-available": "有新版本可用", "next-page": "下一页", @@ -94,11 +105,14 @@ "platform": "平台", "platform-created": "平台 {name} 已创建", "platforms": "平台", - "platforms-n": "{n} 平台 | {n} 平台", + "platforms-n": "{n} 个平台", + "play-all": "全部播放", + "playlists": "播放列表", "previous-page": "上一页", "primary-navigation": "主导航", "profile": "简介", "random": "随机", + "recently-added-soundtracks": "最近添加的原声音乐", "remove": "移除", "removing-from-filesystem": "正在从系统删除", "required": "必填", @@ -116,11 +130,17 @@ "server-stats": "服务器状态", "settings": "设置", "show-password": "显示密码", + "shuffle": "随机播放", "size": "容量", "size-on-disk": "磁盘大小", "slug": "Slug", "smart-collection": "智能收藏", "smart-collections": "智能收藏", + "soundtrack-favorite-added": "曲目已添加到收藏", + "soundtrack-favorite-failed": "无法更新收藏曲目", + "soundtrack-favorite-removed": "曲目已从收藏中移除", + "soundtracks-by-genre": "按类型浏览原声音乐", + "soundtracks-by-platform": "按平台浏览原声音乐", "state": "状态", "states": "状态", "states-n": "{n} 状态 | {n} 状态", diff --git a/frontend/src/locales/zh_TW/common.json b/frontend/src/locales/zh_TW/common.json index 6e61244254..89214fa2d5 100644 --- a/frontend/src/locales/zh_TW/common.json +++ b/frontend/src/locales/zh_TW/common.json @@ -8,11 +8,13 @@ "activity": "動態", "add": "新增", "administration": "管理系統", + "albums-n": "{n} 張專輯", "all": "全部", "all-files-skipped": "所有檔案皆已跳過,未上傳任何內容。", "all-platforms": "所有平台", "and": "和", "apply": "套用", + "artists-n": "{n} 位藝人", "ascii-only": "僅限 ASCII 字元", "back": "返回", "back-btn": "返回", @@ -42,6 +44,8 @@ "core": "核心", "create": "建立", "crt-mode": "CRT mode", + "decade-mix": "年代混音", + "decades-n": "{n} 個年代", "delete": "刪除", "details": "詳細資訊", "discard": "捨棄", @@ -54,20 +58,24 @@ "edit": "編輯", "exclude-on-delete": "刪除時排除", "expand": "展開", + "favorite-soundtracks": "收藏的原聲音樂", "filter": "篩選", "firmware": "韌體", "first-page": "第一頁", "for": "用於", + "free-radio": "自由電台", "games": "遊戲", "games-header": "遊戲", "games-n": "{n} 遊戲 | {n} 遊戲", "generate": "產生", + "genres-n": "{n} 個類型", "got-it": "我知道了", "hide-password": "隱藏密碼", "home": "首頁", "in-library": "已在遊戲庫", "invalid-email": "無效的電子郵件", "invalid-name": "無效的名稱", + "jukebox": "音樂盒", "kind": "種類", "last-page": "最後一頁", "last-updated": "最後更新", @@ -79,6 +87,9 @@ "local": "本地", "logout": "登出", "logs": "Logs", + "minutes-n": "{n} 分鐘", + "music-by-album": "依專輯瀏覽原聲音樂", + "music-by-artist": "依藝人瀏覽原聲音樂", "name": "名稱", "new-version-available": "有新版本可用", "next-page": "下一頁", @@ -94,11 +105,14 @@ "platform": "平台", "platform-created": "平台 {name} 已建立", "platforms": "平台", - "platforms-n": "{n} 平台 | {n} 平台", + "platforms-n": "{n} 個平台", + "play-all": "全部播放", + "playlists": "播放清單", "previous-page": "上一頁", "primary-navigation": "主要導覽", "profile": "用戶資料", "random": "隨機", + "recently-added-soundtracks": "最近新增的原聲音樂", "remove": "移除", "removing-from-filesystem": "正在從系統移除", "required": "必填", @@ -116,11 +130,17 @@ "server-stats": "伺服器狀態", "settings": "設定", "show-password": "顯示密碼", + "shuffle": "隨機播放", "size": "大小", "size-on-disk": "占用磁碟空間", "slug": "網址關鍵字", "smart-collection": "智能收藏庫", "smart-collections": "智能收藏庫", + "soundtrack-favorite-added": "曲目已加入收藏", + "soundtrack-favorite-failed": "無法更新收藏曲目", + "soundtrack-favorite-removed": "曲目已從收藏中移除", + "soundtracks-by-genre": "依類型瀏覽原聲音樂", + "soundtracks-by-platform": "依平台瀏覽原聲音樂", "state": "即時存檔", "states": "即時存檔", "states-n": "{n} 即時存檔 | {n} 即時存檔", diff --git a/frontend/src/plugins/router.ts b/frontend/src/plugins/router.ts index 1fe6facee8..db9a209c93 100644 --- a/frontend/src/plugins/router.ts +++ b/frontend/src/plugins/router.ts @@ -25,6 +25,7 @@ export const ROUTES = { MAIN: "main", HOME: "home", SEARCH: "search", + MUSIC: "music", PLATFORM: "platform", COLLECTION: "collection", VIRTUAL_COLLECTION: "virtual-collection", @@ -186,6 +187,15 @@ const routes = [ v2: v2For(ROUTES.SEARCH), }, }, + { + path: "music", + name: ROUTES.MUSIC, + meta: { title: i18n.global.t("common.jukebox") }, + components: { + default: () => import("@/views/Home.vue"), + v2: v2For(ROUTES.MUSIC), + }, + }, { path: "platform/:platform", name: ROUTES.PLATFORM, diff --git a/frontend/src/services/api/music.ts b/frontend/src/services/api/music.ts new file mode 100644 index 0000000000..1163543e3b --- /dev/null +++ b/frontend/src/services/api/music.ts @@ -0,0 +1,67 @@ +import type { + MusicPage_MusicTrackSchema_, + MusicTrackIdsPayload, + MusicTrackSchema, +} from "@/__generated__"; +import api from "@/services/api"; + +export interface MusicTrackFilters { + search?: string; + limit?: number; + offset?: number; + romId?: number; + orderBy?: + | "title" + | "artist" + | "album" + | "duration" + | "year" + | "platform" + | "added"; + orderDir?: "asc" | "desc"; +} + +async function getTracks(filters: MusicTrackFilters = {}) { + return api.get("/music/tracks", { + params: { + search: filters.search || undefined, + limit: filters.limit ?? 10_000, + offset: filters.offset ?? 0, + rom_id: filters.romId, + order_by: filters.orderBy ?? "title", + order_dir: filters.orderDir ?? "asc", + }, + }); +} + +const ALL_TRACKS_PAGE_SIZE = 1_000; + +async function getAllTracks( + filters: Omit = {}, +): Promise { + const tracks: MusicTrackSchema[] = []; + let total = Number.POSITIVE_INFINITY; + + while (tracks.length < total) { + const { data } = await getTracks({ + ...filters, + limit: ALL_TRACKS_PAGE_SIZE, + offset: tracks.length, + }); + tracks.push(...data.items); + total = data.total; + if (data.items.length === 0) break; + } + + return tracks; +} + +async function addFavorites(payload: MusicTrackIdsPayload) { + return api.post<{ added: number }>("/music/favorites", payload); +} + +async function removeFavorites(payload: MusicTrackIdsPayload) { + return api.delete<{ removed: number }>("/music/favorites", { data: payload }); +} + +export default { getTracks, getAllTracks, addFavorites, removeFavorites }; diff --git a/frontend/src/stores/soundtrackPlayer.ts b/frontend/src/stores/soundtrackPlayer.ts index 16ada5f5cd..aeceb40f0c 100644 --- a/frontend/src/stores/soundtrackPlayer.ts +++ b/frontend/src/stores/soundtrackPlayer.ts @@ -3,6 +3,8 @@ import { throttle } from "lodash"; import { defineStore } from "pinia"; import { computed, ref, shallowRef } from "vue"; import type { TrackMetaSchema } from "@/__generated__"; +import type { DetailedRom } from "@/stores/roms"; +import { FRONTEND_RESOURCES_PATH } from "@/utils"; const volumeStorage = useLocalStorage("soundtrack.volume", 1); const mutedStorage = useLocalStorage("soundtrack.muted", false); @@ -25,8 +27,28 @@ export type PlayerMeta = { duration?: number; coverUrl?: string; folderCoverUrl?: string; + gameArtworkUrl?: string; }; +export type SoundtrackArtworkRom = Pick< + DetailedRom, + "ss_metadata" | "path_cover_large" | "path_cover_small" | "url_cover" +>; + +export function resolveSoundtrackGameArtwork( + rom: SoundtrackArtworkRom, +): string | undefined { + const logoPath = rom.ss_metadata?.logo_path; + if (logoPath) return `${FRONTEND_RESOURCES_PATH}/${logoPath}`; + + return ( + rom.path_cover_large ?? + rom.path_cover_small ?? + rom.url_cover ?? + undefined + ); +} + const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { const track = ref(null); const meta = ref({}); @@ -39,6 +61,8 @@ const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { const volume = volumeStorage; const muted = mutedStorage; const playlist = ref([]); + const originalPlaylist = ref([]); + const isShuffled = ref(false); const playlistMeta = ref>({}); const activePlaylistRomId = ref(null); @@ -94,14 +118,73 @@ const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { isBuffering.value = false; } - function loadPlaylistForRom( - romId: number, + function shuffledTracks(tracks: PlayerTrack[]): PlayerTrack[] { + const result = [...tracks]; + for (let index = result.length - 1; index > 0; index -= 1) { + const target = Math.floor(Math.random() * (index + 1)); + [result[index], result[target]] = [result[target], result[index]]; + } + return result; + } + + function loadPlaylist( tracks: PlayerTrack[], metas: Record, + romId: number | null = null, + preserveShuffle = false, ) { - playlist.value = tracks; + const previousOrder = playlist.value; + const wasShuffled = isShuffled.value; + originalPlaylist.value = [...tracks]; playlistMeta.value = metas; activePlaylistRomId.value = romId; + + if (preserveShuffle && wasShuffled) { + const tracksByKey = new Map( + tracks.map((item) => [item.romId + ":" + item.fileId, item]), + ); + const restored = previousOrder.flatMap((item) => { + const next = tracksByKey.get(item.romId + ":" + item.fileId); + if (!next) return []; + tracksByKey.delete(item.romId + ":" + item.fileId); + return [next]; + }); + playlist.value = + restored.length > 0 + ? [...restored, ...tracksByKey.values()] + : shuffledTracks([...tracksByKey.values()]); + return; + } + + playlist.value = [...tracks]; + isShuffled.value = false; + } + + function loadPlaylistForRom( + romId: number, + tracks: PlayerTrack[], + metas: Record, + ) { + loadPlaylist(tracks, metas, romId); + } + + function toggleShuffle() { + if (isShuffled.value) { + playlist.value = [...originalPlaylist.value]; + isShuffled.value = false; + return; + } + + const current = track.value; + const remaining = originalPlaylist.value.filter( + (item) => + !current || + item.fileId !== current.fileId || + item.romId !== current.romId, + ); + const shuffled = shuffledTracks(remaining); + playlist.value = current ? [current, ...shuffled] : shuffled; + isShuffled.value = true; } function play(t: PlayerTrack, m: PlayerMeta) { @@ -161,7 +244,9 @@ const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { currentTime.value = 0; duration.value = 0; playlist.value = []; + originalPlaylist.value = []; playlistMeta.value = {}; + isShuffled.value = false; activePlaylistRomId.value = null; } @@ -191,6 +276,7 @@ const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { volume, muted, playlist, + isShuffled, activePlaylistRomId, hasPrevious, hasNext, @@ -202,12 +288,14 @@ const useSoundtrackPlayer = defineStore("soundtrackPlayer", () => { seek, setVolume, toggleMute, + toggleShuffle, setPlaying, setBuffering, setDuration, setError, next, previous, + loadPlaylist, loadPlaylistForRom, reportCurrentTime, }; diff --git a/frontend/src/v2/components/AppShell/UserMenu.vue b/frontend/src/v2/components/AppShell/UserMenu.vue index 0be6e99c8e..611d8e2b8b 100644 --- a/frontend/src/v2/components/AppShell/UserMenu.vue +++ b/frontend/src/v2/components/AppShell/UserMenu.vue @@ -7,7 +7,7 @@ // • Library — Library management, Scan settings, Metadata sources, // Client API tokens // • System — Administration, Server stats -// • Tools — Controller debug +// • Tools — Jukebox, Controller debug // • Actions — Scan, Upload (librarian actions, not settings) // • About / Changelog — kept as dialogs (no dedicated views) // • Log out @@ -272,6 +272,18 @@ async function onLogout() {
{{ t("settings.group-tools") }}
+ + + (); const emit = defineEmits<{ (e: "upload-tracks"): void; - (e: "delete-track", fileId: number): void; + (e: "delete-track", fileId: number, romId: number): void; + (e: "favorite-track", fileId: number, isFavorite: boolean): void; }>(); const { t } = useI18n(); const snackbar = useSnackbar(); +const canEditPlaylists = useCan("playlist.edit"); const player = useSoundtrackPlayer(); const { @@ -62,8 +80,10 @@ const { isBuffering, currentTime, duration, + playlist, hasPrevious, hasNext, + isShuffled, } = storeToRefs(player); // ---------- Track + cover discovery ---------- @@ -87,17 +107,100 @@ function fileUrl(fileId: number, fileName: string): string { return `/api/roms/${fileId}/files/content/${encodeURIComponent(fileName)}`; } -const tracks = computed(() => - (props.rom.files ?? []) +interface PanelTrack { + id: number; + romId: number; + fileName: string; + fileSizeBytes?: number; + url: string; + coverUrl?: string; + gameArtworkUrl?: string; + context?: string; + isFavorite: boolean; +} + +const tracks = computed(() => { + if (props.musicTracks) { + return props.musicTracks.map((track) => ({ + id: track.rom_file_id, + romId: track.rom_id, + fileName: track.title || track.game_name || String(track.rom_file_id), + url: track.stream_url, + coverUrl: track.cover_url || undefined, + gameArtworkUrl: track.game_cover_url || undefined, + context: [track.game_name, track.platform_name].filter(Boolean).join(" · "), + isFavorite: Boolean(track.is_favorite), + })); + } + + return (props.rom?.files ?? []) .filter( - (f) => f.category === "soundtrack" && AUDIO_EXTS.has(getExt(f.file_name)), + (file) => + file.category === "soundtrack" && AUDIO_EXTS.has(getExt(file.file_name)), ) .slice() - .sort((a, b) => a.file_name.localeCompare(b.file_name)), -); + .sort((a, b) => a.file_name.localeCompare(b.file_name)) + .map((track) => ({ + id: track.id, + romId: props.rom!.id, + fileName: track.file_name, + fileSizeBytes: track.file_size_bytes, + url: fileUrl(track.id, track.file_name), + isFavorite: false, + })); +}); + +const favoriteOverrides = ref>(new Map()); +const updatingFavorites = ref>(new Set()); + +function isTrackFavorite(track: PanelTrack): boolean { + return favoriteOverrides.value.get(track.id) ?? track.isFavorite; +} + +async function toggleFavorite(track: PanelTrack) { + if (updatingFavorites.value.has(track.id)) return; + const nextFavorite = !isTrackFavorite(track); + updatingFavorites.value = new Set(updatingFavorites.value).add(track.id); + try { + const payload = { rom_file_ids: [track.id] }; + if (nextFavorite) await musicApi.addFavorites(payload); + else await musicApi.removeFavorites(payload); + favoriteOverrides.value = new Map(favoriteOverrides.value).set( + track.id, + nextFavorite, + ); + emit("favorite-track", track.id, nextFavorite); + snackbar.success( + t( + nextFavorite + ? "common.soundtrack-favorite-added" + : "common.soundtrack-favorite-removed", + ), + { icon: nextFavorite ? "mdi-heart" : "mdi-heart-outline" }, + ); + } catch { + snackbar.error(t("common.soundtrack-favorite-failed"), { + icon: "mdi-alert-circle-outline", + }); + } finally { + const pending = new Set(updatingFavorites.value); + pending.delete(track.id); + updatingFavorites.value = pending; + } +} + +const displayedTracks = computed(() => { + if (!props.startShuffled || !isShuffled.value) return tracks.value; + const byId = new Map( + tracks.value.map((track) => [`${track.romId}:${track.id}`, track]), + ); + return playlist.value + .map((track) => byId.get(`${track.romId}:${track.fileId}`)) + .filter((track): track is PanelTrack => Boolean(track)); +}); const folderCoverUrl = computed(() => { - const cover = (props.rom.files ?? []) + const cover = (props.rom?.files ?? []) .filter( (f) => f.category === "soundtrack" && COVER_EXTS.has(getExt(f.file_name)), ) @@ -105,17 +208,25 @@ const folderCoverUrl = computed(() => { return cover ? fileUrl(cover.id, cover.file_name) : null; }); +const gameArtworkUrl = computed(() => + props.rom ? resolveSoundtrackGameArtwork(props.rom) : undefined, +); + // ---------- Metadata fetch ---------- const tracksMeta = ref>(new Map()); const isLoadingMeta = ref(false); let metaAbort: AbortController | null = null; +let shouldStartShuffled = Boolean(props.startShuffled); function coverUrlForMeta(m: TrackMetaSchema | undefined): string | null { if (m?.cover_path) return `${FRONTEND_RESOURCES_PATH}/${m.cover_path}`; return null; } -function toPlayerMeta(m: TrackMetaSchema | undefined): PlayerMeta { +function toPlayerMeta( + m: TrackMetaSchema | undefined, + track?: PanelTrack, +): PlayerMeta { return { title: m?.title ?? undefined, artist: m?.artist ?? undefined, @@ -125,27 +236,50 @@ function toPlayerMeta(m: TrackMetaSchema | undefined): PlayerMeta { track: m?.track ?? undefined, disc: m?.disc ?? undefined, duration: m?.duration_seconds ?? undefined, - coverUrl: coverUrlForMeta(m) ?? undefined, + coverUrl: track?.coverUrl ?? coverUrlForMeta(m) ?? undefined, folderCoverUrl: folderCoverUrl.value ?? undefined, + gameArtworkUrl: track?.gameArtworkUrl ?? gameArtworkUrl.value, }; } -function syncStorePlaylist() { - if (player.activePlaylistRomId !== props.rom.id) return; +function syncOrStartPlaylist() { + const selectedTrackIsAvailable = + tracks.value.some( + (track) => + track.id === activeStoreTrack.value?.fileId && + track.romId === activeStoreTrack.value?.romId, + ); + if (!selectedTrackIsAvailable) { + const firstTrack = tracks.value[0]; + if (firstTrack) selectTrack(firstTrack.id); + return; + } const playerTracks: PlayerTrack[] = tracks.value.map((t) => ({ - romId: props.rom.id, + romId: t.romId, fileId: t.id, - fileName: t.file_name, - url: fileUrl(t.id, t.file_name), + fileName: t.fileName, + url: t.url, })); const metas: Record = {}; for (const t of tracks.value) { - metas[t.id] = toPlayerMeta(tracksMeta.value.get(t.id)); + metas[t.id] = toPlayerMeta(tracksMeta.value.get(t.id), t); + } + player.loadPlaylist(playerTracks, metas, props.rom?.id, true); + if (shouldStartShuffled) { + if (!isShuffled.value) player.toggleShuffle(); + shouldStartShuffled = false; } - player.loadPlaylistForRom(props.rom.id, playerTracks, metas); } async function loadAllMetadata() { + if (props.musicTracks) { + tracksMeta.value = new Map( + props.musicTracks.map((track) => [track.rom_file_id, track]), + ); + syncOrStartPlaylist(); + return; + } + if (!props.rom) return; metaAbort?.abort(); metaAbort = new AbortController(); isLoadingMeta.value = true; @@ -159,7 +293,18 @@ async function loadAllMetadata() { if (row.track_meta) next.set(row.file_id, row.track_meta); } tracksMeta.value = next; - syncStorePlaylist(); + try { + const { data: catalog } = await musicApi.getTracks({ romId: props.rom.id }); + favoriteOverrides.value = new Map( + catalog.items.map((track) => [ + track.rom_file_id, + Boolean(track.is_favorite), + ]), + ); + } catch { + // Metadata playback remains available if favorite state cannot load. + } + syncOrStartPlaylist(); } catch (err: unknown) { const maybeCfg = err as { config?: AxiosRequestConfig }; if (axios.isCancel(err) || maybeCfg.config?.signal?.aborted) return; @@ -167,6 +312,7 @@ async function loadAllMetadata() { icon: "mdi-alert", timeout: 3000, }); + syncOrStartPlaylist(); } finally { isLoadingMeta.value = false; } @@ -178,7 +324,7 @@ onMounted(() => { // Refetch when the rom updates (new tracks, re-tagged files, etc.). watch( - () => props.rom.updated_at, + () => [props.rom?.updated_at, props.musicTracks] as const, () => void loadAllMetadata(), ); @@ -188,7 +334,12 @@ onBeforeUnmount(() => { // ---------- Active-track + derived metadata ---------- const activeTrackId = computed(() => - activeStoreTrack.value && activeStoreTrack.value.romId === props.rom.id + activeStoreTrack.value && + tracks.value.some( + (track) => + track.id === activeStoreTrack.value?.fileId && + track.romId === activeStoreTrack.value?.romId, + ) ? activeStoreTrack.value.fileId : null, ); @@ -197,6 +348,15 @@ const activeTrack = computed(() => tracks.value.find((t) => t.id === activeTrackId.value), ); +const panelRoot = ref(null); +watch(activeTrackId, async (fileId, previousFileId) => { + if (fileId == null || fileId === previousFileId) return; + await nextTick(); + panelRoot.value + ?.querySelector(`[data-track-id="${fileId}"]`) + ?.scrollIntoView({ block: "nearest" }); +}); + const activeMeta = computed(() => activeTrackId.value != null ? tracksMeta.value.get(activeTrackId.value) @@ -205,8 +365,10 @@ const activeMeta = computed(() => const activeArtUrl = computed( () => + activeTrack.value?.coverUrl ?? coverUrlForMeta(activeMeta.value) ?? folderCoverUrl.value ?? + gameArtworkUrl.value ?? "/assets/default/album_cover.jpg", ); @@ -214,7 +376,7 @@ const activeTitle = computed( () => activeMeta.value?.title ?? (activeTrack.value - ? activeTrack.value.file_name.replace(/\.[^.]+$/, "") + ? activeTrack.value.fileName.replace(/\.[^.]+$/, "") : ""), ); @@ -227,10 +389,11 @@ function trackTitleFor(fileId: number, fallback: string): string { function trackSubtitleFor(fileId: number): string { const m = tracksMeta.value.get(fileId); - if (!m) return ""; const parts: string[] = []; - if (m.artist) parts.push(m.artist); - if (m.album) parts.push(m.album); + if (m?.artist) parts.push(m.artist); + if (m?.album) parts.push(m.album); + const context = tracks.value.find((track) => track.id === fileId)?.context; + if (context) parts.push(context); return parts.join(" · "); } @@ -268,24 +431,6 @@ function headerChips(meta: TrackMetaSchema | undefined): ChipItem[] { return items; } -// Compact chips shown on each track row — keep it tight so rows don't bloat. -function rowChips(fileId: number): ChipItem[] { - const meta = tracksMeta.value.get(fileId); - if (!meta) return []; - const items: ChipItem[] = []; - if (meta.year) - items.push({ - icon: "mdi-calendar", - label: String(meta.year), - color: "accent", - }); - if (meta.genre) - items.push({ icon: "mdi-music-clef-treble", label: meta.genre }); - if (meta.track) items.push({ icon: "mdi-numeric", label: `#${meta.track}` }); - if (meta.disc) items.push({ icon: "mdi-disc", label: String(meta.disc) }); - return items; -} - // ---------- Totals ---------- const totalDurationSeconds = computed(() => { let total = 0; @@ -302,32 +447,35 @@ function selectTrack(fileId: number) { if (!target) return; const playerTracks: PlayerTrack[] = tracks.value.map((t) => ({ - romId: props.rom.id, + romId: t.romId, fileId: t.id, - fileName: t.file_name, - url: fileUrl(t.id, t.file_name), + fileName: t.fileName, + url: t.url, })); const metas: Record = {}; for (const t of tracks.value) { - metas[t.id] = toPlayerMeta(tracksMeta.value.get(t.id)); + metas[t.id] = toPlayerMeta(tracksMeta.value.get(t.id), t); } - player.loadPlaylistForRom(props.rom.id, playerTracks, metas); + const wasShuffled = isShuffled.value; + player.loadPlaylist(playerTracks, metas, props.rom?.id, wasShuffled); const entry = playerTracks.find((p) => p.fileId === fileId)!; player.play(entry, metas[fileId]); + if (shouldStartShuffled && !isShuffled.value) player.toggleShuffle(); + shouldStartShuffled = false; } -function onDelete(fileId: number) { - if (activeTrackId.value === fileId) player.stop(); - emit("delete-track", fileId); +function onDelete(track: PanelTrack) { + if (activeTrackId.value === track.id) player.stop(); + emit("delete-track", track.id, track.romId); } // Mirror the saves/states pattern: synthesize an anchor click against // the file content endpoint so the browser routes the download with // the original filename instead of opening it in a new tab. -function downloadTrack(track: { id: number; file_name: string }) { +function downloadTrack(track: PanelTrack) { const a = document.createElement("a"); - a.href = fileUrl(track.id, track.file_name); - a.download = track.file_name; + a.href = track.url; + a.download = track.fileName; document.body.appendChild(a); a.click(); a.remove(); @@ -352,19 +500,19 @@ function seekValueText(v: number): string {