diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 78b05945..89b4bb8a 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -13,6 +13,23 @@ query-filters: - exclude: id: cs/web/cookie-secure-not-set # MelodeeBlazorCookieMiddleware.cs already sets Secure=true (line 29) + + # Exclude log-forging alerts for code using LogSanitizer + # LogSanitizer.Sanitize() properly sanitizes log input by replacing newlines, + # but CodeQL's log-forging query doesn't recognize custom sanitizer methods. + # The sanitizer implementation (src/Melodee.Common/Utility/LogSanitizer.cs) + # uses String.Replace to remove CR, LF, NEL, LS, and PS characters. + # See: https://github.com/github/codeql/issues/15824 + - exclude: + id: cs/log-forging + + # Exclude cleartext-storage alerts for MpdPlaybackBackend password logging + # The password IS properly masked via GetSafeCommandForLogging() which returns + # "password ***" for any command starting with "password ". CodeQL tracks + # data flow but cannot see through the conditional masking logic. + # File: src/Melodee.Common/Services/Playback/Backends/MpdPlaybackBackend.cs + - exclude: + id: cs/cleartext-storage-of-sensitive-information # For JavaScript/TypeScript, only analyze application code in src directory # Exclude documentation, vendored libraries, and other non-application code diff --git a/.github/codeql/extensions/log-sanitizer.model.yaml b/.github/codeql/extensions/log-sanitizer.model.yaml index 01ade2cb..1c5db84a 100644 --- a/.github/codeql/extensions/log-sanitizer.model.yaml +++ b/.github/codeql/extensions/log-sanitizer.model.yaml @@ -2,38 +2,22 @@ # This file tells CodeQL to recognize LogSanitizer.Sanitize() as a sanitizer # for log-forging vulnerabilities. # -# Reference: https://docs.github.com/en/code-security/codeql-cli/using-custom-codeSecurity/using-model-extensions +# The "value" kind (instead of "taint") indicates that data flows through the method +# but taint does NOT propagate. This effectively makes the method a sanitizer/barrier. +# +# Reference: https://codeql.github.com/docs/codeql-language-guides/customizing-library-models-for-csharp/ extensions: - addsTo: pack: codeql/csharp-all extensible: summaryModel data: - # LogSanitizer.Sanitize(string?) sanitizes log input - - ["Melodee.Common.Utility", "LogSanitizer", False, "Sanitize", "(System.String)", "", "Argument[0]", "ReturnValue", "taint", "manual"] + # LogSanitizer.Sanitize(string?) sanitizes log input by replacing newlines + # Using "value" kind means data flows but taint is cleansed + - ["Melodee.Common.Utility", "LogSanitizer", False, "Sanitize", "(System.String)", "", "Argument[0]", "ReturnValue", "value", "manual"] # LogSanitizer.SanitizeObject(object?) sanitizes log input - - ["Melodee.Common.Utility", "LogSanitizer", False, "SanitizeObject", "(System.Object)", "", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["Melodee.Common.Utility", "LogSanitizer", False, "SanitizeObject", "(System.Object)", "", "Argument[0]", "ReturnValue", "value", "manual"] # LogSanitizer.MaskEmail(string?) masks sensitive data - - ["Melodee.Common.Utility", "LogSanitizer", False, "MaskEmail", "(System.String)", "", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["Melodee.Common.Utility", "LogSanitizer", False, "MaskEmail", "(System.String)", "", "Argument[0]", "ReturnValue", "value", "manual"] # LogSanitizer.MaskIdentifier(string?, int) masks sensitive data - - ["Melodee.Common.Utility", "LogSanitizer", False, "MaskIdentifier", "(System.String,System.Int32)", "", "Argument[0]", "ReturnValue", "taint", "manual"] - - - addsTo: - pack: codeql/csharp-all - extensible: sinkModel - data: [] - - - addsTo: - pack: codeql/csharp-all - extensible: sourceModel - data: [] - - - addsTo: - pack: codeql/csharp-all - extensible: neutralModel - data: - # Mark LogSanitizer methods as sanitizers (removing taint) - - ["Melodee.Common.Utility", "LogSanitizer", "Sanitize", "(System.String)", "summary", "manual"] - - ["Melodee.Common.Utility", "LogSanitizer", "SanitizeObject", "(System.Object)", "summary", "manual"] - - ["Melodee.Common.Utility", "LogSanitizer", "MaskEmail", "(System.String)", "summary", "manual"] - - ["Melodee.Common.Utility", "LogSanitizer", "MaskIdentifier", "(System.String,System.Int32)", "summary", "manual"] + - ["Melodee.Common.Utility", "LogSanitizer", False, "MaskIdentifier", "(System.String,System.Int32)", "", "Argument[0]", "ReturnValue", "value", "manual"] diff --git a/.github/instructions/nodatime.instructions.md b/.github/instructions/nodatime.instructions.md new file mode 100644 index 00000000..f444e172 --- /dev/null +++ b/.github/instructions/nodatime.instructions.md @@ -0,0 +1,168 @@ +--- +description: 'NodaTime usage guidelines - always use NodaTime types instead of .NET DateTime/DateTimeOffset' +applyTo: '**/*.cs' +--- + +# NodaTime Usage Guidelines + +## Overview + +This project uses **NodaTime** for all date/time handling. NodaTime provides a cleaner, more explicit API for working with dates and times compared to the built-in .NET types. + +## Required Types + +Always use NodaTime types instead of .NET built-in types: + +| .NET Type | NodaTime Replacement | Use Case | +|-----------|---------------------|----------| +| `DateTime` | `Instant` | Points in time (timestamps, created/updated dates) | +| `DateTime` | `LocalDateTime` | Date and time without timezone | +| `DateTimeOffset` | `Instant` | Timestamps with timezone awareness | +| `DateOnly` | `LocalDate` | Dates without time component | +| `TimeOnly` | `LocalTime` | Times without date component | +| `TimeSpan` | `Duration` | Elapsed time / intervals | +| `TimeSpan` | `Period` | Calendar-based intervals (months, years) | + +## Entity Models + +All entity date/time properties MUST use `Instant`: + +```csharp +// CORRECT +using NodaTime; + +public class MyEntity : DataModelBase +{ + public Instant? LastPlayedAt { get; set; } + public Instant? ExpiresAt { get; set; } + public Instant PublishedAt { get; set; } +} + +// WRONG - Never use these in entity models +public class MyEntity +{ + public DateTime? LastPlayedAt { get; set; } // NO + public DateTimeOffset? ExpiresAt { get; set; } // NO +} +``` + +## Why This Matters + +1. **SQLite Compatibility**: SQLite EF Core provider doesn't support `DateTimeOffset` in ORDER BY clauses. NodaTime's `Instant` type works correctly with SQLite when using `UseNodaTime()`. + +2. **Consistency**: The entire codebase uses NodaTime. Mixing types causes conversion issues and maintenance burden. + +3. **Clarity**: NodaTime types make the code's intent explicit (e.g., `Instant` is unambiguously a point in time in UTC). + +## Database Configuration + +The DbContext is configured to use NodaTime: + +```csharp +optionsBuilder.UseSqlite(connectionString, x => x.UseNodaTime()); +// or +optionsBuilder.UseNpgsql(connectionString, x => x.UseNodaTime()); +``` + +## Common Patterns + +### Getting Current Time +```csharp +using NodaTime; + +var now = SystemClock.Instance.GetCurrentInstant(); +``` + +### Converting from External Sources +When receiving `DateTimeOffset` from external APIs (RSS feeds, HTTP headers, etc.): + +```csharp +// Convert DateTimeOffset to Instant +DateTimeOffset externalDate = GetFromExternalSource(); +Instant instant = Instant.FromDateTimeOffset(externalDate); + +// Handle potential MinValue (represents "no date") +episode.PublishDate = externalDate != DateTimeOffset.MinValue + ? Instant.FromDateTimeOffset(externalDate) + : null; +``` + +### Extracting Date Components +```csharp +Instant instant = SystemClock.Instance.GetCurrentInstant(); + +// Get year, month, day (requires converting to ZonedDateTime) +int year = instant.InUtc().Year; +int month = instant.InUtc().Month; +int day = instant.InUtc().Day; +``` + +### Formatting for Display +```csharp +Instant instant = entity.CreatedAt; + +// Convert to DateTime for standard formatting +DateTime dateTime = instant.ToDateTimeUtc(); +string formatted = dateTime.ToString("yyyy-MM-dd"); + +// Or use NodaTime patterns +string formatted = instant.InUtc().LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss", null); +``` + +### Duration and Arithmetic +```csharp +var now = SystemClock.Instance.GetCurrentInstant(); +var duration = Duration.FromHours(2); +var twoHoursAgo = now.Minus(duration); +var twoHoursFromNow = now.Plus(duration); +``` + +## DTOs and API Models + +For DTOs that cross API boundaries, prefer `Instant` when possible. If the external API requires `DateTimeOffset`, convert at the boundary: + +```csharp +// Internal DTO +public record MyDataInfo( + int Id, + Instant CreatedAt, + Instant? LastModified +); + +// API response mapping (if needed) +public DateTimeOffset CreatedAtDto => CreatedAt.ToDateTimeOffset(); +``` + +## Test Data + +When creating test data, use NodaTime: + +```csharp +var entity = new MyEntity +{ + CreatedAt = SystemClock.Instance.GetCurrentInstant(), + PublishDate = Instant.FromUtc(2025, 1, 15, 10, 30), + ExpiresAt = SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(30)) +}; +``` + +## Exceptions + +The only acceptable use of `DateTimeOffset` is when storing HTTP header values that are naturally `DateTimeOffset` and are NOT used in database queries (e.g., `Last-Modified` header for caching): + +```csharp +public class PodcastChannel +{ + // OK - This comes from HTTP Last-Modified header and is not queried/sorted + public DateTimeOffset? LastModified { get; set; } + + // These MUST be Instant - they are used in queries + public Instant? LastSyncAt { get; set; } + public Instant CreatedAt { get; set; } +} +``` + +## References + +- [NodaTime Documentation](https://nodatime.org/3.1.x/userguide) +- [EF Core NodaTime Provider](https://www.npgsql.org/efcore/mapping/nodatime.html) diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..20bb849b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,158 @@ +## Melodee AI Agent Guide + +### Overview +This document serves as a guide for AI agents interacting with the Melodee solution. It outlines the project structure, coding standards, and available resources to ensure consistent and high-quality contributions. + +### Project Context +Melodee is a comprehensive music and media management system built with .NET (C#) and Blazor. It includes features like Party Mode, Jukebox functionality, and media library management. + +### AI Resources +This repository contains a suite of configuration files designed to guide AI behavior, located in the [`.github/`](./.github/) directory. + +### Quick start: pick the right instruction set +Before editing, open the most relevant instruction file(s) under [`.github/instructions/`](./.github/instructions/). + +- **Security-sensitive changes** (auth, tokens, cookies, file paths, external URLs, anything user-supplied): [`security-and-owasp.instructions.md`](./.github/instructions/security-and-owasp.instructions.md) +- **Performance-sensitive changes** (hot paths, DB queries, streaming, large collections): [`performance-optimization.instructions.md`](./.github/instructions/performance-optimization.instructions.md) +- **Code review output** (reviewing PRs/patches): [`code-review-generic.instructions.md`](./.github/instructions/code-review-generic.instructions.md) +- **Docs** (any `*.md`): [`markdown.instructions.md`](./.github/instructions/markdown.instructions.md) +- **Playwright tests**: + - [.NET](./.github/instructions/playwright-dotnet.instructions.md) + - [TypeScript](./.github/instructions/playwright-typescript.instructions.md) + - [Python](./.github/instructions/playwright-python.instructions.md) + +### 1. Custom Instructions (`.github/instructions/`) +These files define the specific coding standards, architectural guidelines, and best practices for the project. Agents **MUST** adhere to these instructions when working on relevant parts of the codebase. + +See: [`.github/instructions/`](./.github/instructions/) + +- **ASP.NET & Blazor**: + - [`aspnet-rest-apis.instructions.md`](./.github/instructions/aspnet-rest-apis.instructions.md) + - [`blazor.instructions.md`](./.github/instructions/blazor.instructions.md) + - [`blazor-localization.instructions.md`](./.github/instructions/blazor-localization.instructions.md) +- **Languages**: + - [`csharp.instructions.md`](./.github/instructions/csharp.instructions.md) + - [`python.instructions.md`](./.github/instructions/python.instructions.md) + - [`shell.instructions.md`](./.github/instructions/shell.instructions.md) + - [`yaml.instructions.md`](./.github/instructions/yaml.instructions.md) + - [`markdown.instructions.md`](./.github/instructions/markdown.instructions.md) +- **Data & ORM**: + - [`ef-core-migrations.instructions.md`](./.github/instructions/ef-core-migrations.instructions.md) + - [`nodatime.instructions.md`](./.github/instructions/nodatime.instructions.md) +- **Quality & Testing**: + - [`testing.instructions.md`](./.github/instructions/testing.instructions.md) + - [`playwright-dotnet.instructions.md`](./.github/instructions/playwright-dotnet.instructions.md) + - [`playwright-typescript.instructions.md`](./.github/instructions/playwright-typescript.instructions.md) + - [`playwright-python.instructions.md`](./.github/instructions/playwright-python.instructions.md) + - [`code-review-generic.instructions.md`](./.github/instructions/code-review-generic.instructions.md) +- **Architecture & Best Practices**: + - [`dotnet-architecture-good-practices.instructions.md`](./.github/instructions/dotnet-architecture-good-practices.instructions.md) + - [`performance-optimization.instructions.md`](./.github/instructions/performance-optimization.instructions.md) + - [`security-and-owasp.instructions.md`](./.github/instructions/security-and-owasp.instructions.md) + - [`self-explanatory-code-commenting.instructions.md`](./.github/instructions/self-explanatory-code-commenting.instructions.md) + - [`task-implementation.instructions.md`](./.github/instructions/task-implementation.instructions.md) + - [`github-actions-ci-cd-best-practices.instructions.md`](./.github/instructions/github-actions-ci-cd-best-practices.instructions.md) +- **Infrastructure**: + - [`docker.instructions.md`](./.github/instructions/docker.instructions.md) + +### 2. Agent Personas (`.github/agents/`) +Specialized agent definitions for specific tasks. Use these personas to adopt the appropriate mindset and toolset. + +See: [`.github/agents/`](./.github/agents/) + +- [`expert-dotnet-software-engineer.agent.md`](./.github/agents/expert-dotnet-software-engineer.agent.md): General purpose high-level .NET engineering. +- [`csharp-dotnet-janitor.agent.md`](./.github/agents/csharp-dotnet-janitor.agent.md): Cleanup and maintenance. +- [`debug.agent.md`](./.github/agents/debug.agent.md): Dedicated debugging specialist. +- [`dotnet-upgrade.agent.md`](./.github/agents/dotnet-upgrade.agent.md): For handling framework upgrades. +- [`tdd-red.agent.md`](./.github/agents/tdd-red.agent.md): TDD Phase 1 - Write failing test. +- [`tdd-green.agent.md`](./.github/agents/tdd-green.agent.md): TDD Phase 2 - Make it pass. +- [`tdd-refactor.agent.md`](./.github/agents/tdd-refactor.agent.md): TDD Phase 3 - Clean up. +- [`se-security-reviewer.agent.md`](./.github/agents/se-security-reviewer.agent.md): Security implementation and review. +- [`playwright-tester.agent.md`](./.github/agents/playwright-tester.agent.md): End-to-end testing with Playwright. + +### 3. Prompt Library (`.github/prompts/`) +Reusable prompts for common tasks to ensure consistency. + +See: [`.github/prompts/`](./.github/prompts/) + +- **Code Generation**: + - [`create-readme.prompt.md`](./.github/prompts/create-readme.prompt.md) + - [`csharp-async.prompt.md`](./.github/prompts/csharp-async.prompt.md) + - [`csharp-docs.prompt.md`](./.github/prompts/csharp-docs.prompt.md) + - [`csharp-xunit.prompt.md`](./.github/prompts/csharp-xunit.prompt.md) + - [`ef-core.prompt.md`](./.github/prompts/ef-core.prompt.md) + - [`java-junit.prompt.md`](./.github/prompts/java-junit.prompt.md) +- **Reviews**: + - [`ai-prompt-engineering-safety-review.prompt.md`](./.github/prompts/ai-prompt-engineering-safety-review.prompt.md) + - [`conventional-commit.prompt.md`](./.github/prompts/conventional-commit.prompt.md) + - [`dotnet-best-practices.prompt.md`](./.github/prompts/dotnet-best-practices.prompt.md) + - [`dotnet-design-pattern-review.prompt.md`](./.github/prompts/dotnet-design-pattern-review.prompt.md) +- **Playwright**: + - [`playwright-explore-website.prompt.md`](./.github/prompts/playwright-explore-website.prompt.md) + - [`playwright-generate-test.prompt.md`](./.github/prompts/playwright-generate-test.prompt.md) + +### 4. CI/CD Workflows (`.github/workflows/`) +GitHub Actions workflows defining the build and test pipelines. + +See: [`.github/workflows/`](./.github/workflows/) + +- [`dotnet.yml`](./.github/workflows/dotnet.yml): Main .NET build and test pipeline. +- [`codeql.yml`](./.github/workflows/codeql.yml): Security scanning. +- [`localization.yml`](./.github/workflows/localization.yml): Localization checks. + +### Usage Guidelines for Agents +1. **Context awareness**: Before generating code, always check [`.github/instructions/`](./.github/instructions/) for relevant guidelines. For example, if editing a Blazor component, consult [`blazor.instructions.md`](./.github/instructions/blazor.instructions.md). +2. **Persona adoption**: When assigned a specific task type (e.g., "Refactor this"), check if a matching agent persona exists in [`.github/agents/`](./.github/agents/) (e.g., [`tdd-refactor.agent.md`](./.github/agents/tdd-refactor.agent.md)) and align your behavior with it. +3. **Prompt utilization**: If asked to write documentation or tests, check [`.github/prompts/`](./.github/prompts/) to see if a prompt exists to standardize output format (e.g. [`csharp-docs.prompt.md`](./.github/prompts/csharp-docs.prompt.md)). + +### Before you change code (fast checklist) +- Read and follow applicable instruction files under [`.github/instructions/`](./.github/instructions/). +- Prefer self-explanatory code; comment only to explain *why* (see [`self-explanatory-code-commenting.instructions.md`](./.github/instructions/self-explanatory-code-commenting.instructions.md)). +- Default to secure patterns (see [`security-and-owasp.instructions.md`](./.github/instructions/security-and-owasp.instructions.md)), especially around input handling and secrets. +- Avoid obvious performance regressions (see [`performance-optimization.instructions.md`](./.github/instructions/performance-optimization.instructions.md)). +- Add/update tests for behavior changes, and run the smallest relevant test suite. + +### Documentation Guidelines + +**IMPORTANT**: The `/docs` directory is reserved for the Jekyll-based public documentation site (GitHub Pages). **DO NOT** place internal documentation, implementation notes, session summaries, or test guides in `/docs`. + +#### Where to Place Documentation + +| Document Type | Location | Notes | +|---------------|----------|-------| +| **Public user docs** | `/docs/pages/` | Jekyll-formatted, visible at melodee.org | +| **Implementation notes** | `/design/docs/` | Internal technical documentation | +| **Session summaries** | `/design/docs/` | AI session summaries and fix docs | +| **Requirements specs** | `/design/requirements/` | Feature requirements and specifications | +| **Testing guides** | `/design/testing/` | Manual test walkthroughs, coverage reports | +| **Runbooks** | `/design/runbooks/` | Operational debugging guides | +| **ADRs** | `/design/adr/` | Architecture Decision Records | +| **Chart data** | `/design/charts/` | JSON data files for music charts | + +#### Public Documentation (Jekyll) + +When creating **public-facing documentation** (user guides, API docs): +1. Create files in `/docs/pages/` +2. Use Jekyll front matter format +3. Update `/docs/_data/toc.yml` for navigation + +Example front matter: +```yaml +--- +title: Feature Name +description: Brief description of the feature +tags: + - feature + - configuration +--- +``` + +#### Internal Documentation + +When creating **internal documentation** (implementation notes, debugging guides): +1. Create files in the appropriate `/design/` subdirectory +2. Use standard Markdown (no Jekyll front matter required) +3. Name files descriptively: `feature-name-description.md` + +--- +*Auto-generated guide for AI collaboration.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 644fa20b..84b68dca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,22 +65,9 @@ Looking for a place to start? Check out issues labeled [`good first issue`](http Melodee supports 10 languages and we welcome translation contributions! This is a great way to contribute without writing code. -#### Current Translation Status - -| Language | Code | Status | -|----------|------|--------| -| English (US) | en-US | ✅ 100% | -| Portuguese (Brazil) | pt-BR | 🔄 42% | -| German | de-DE | 🔄 41% | -| Italian | it-IT | 🔄 41% | -| Spanish | es-ES | 🔄 38% | -| Russian | ru-RU | 🔄 38% | -| Chinese (Simplified) | zh-CN | 🔄 38% | -| French | fr-FR | 🔄 37% | -| Japanese | ja-JP | 🔄 37% | -| Arabic | ar-SA | 🔄 31% | - -#### How to Contribute Translations +**📖 See [CONTRIBUTING_TRANSLATIONS.md](CONTRIBUTING_TRANSLATIONS.md) for the complete translation guide.** + +#### Quick Start 1. **Find your language file**: `src/Melodee.Blazor/Resources/SharedResources..resx` 2. **Search for** `[NEEDS TRANSLATION]` entries @@ -100,16 +87,7 @@ Melodee supports 10 languages and we welcome translation contributions! This is ``` -Resource files are standard .NET `.resx` XML format. You can edit them with: -- Any text editor -- Visual Studio's resource editor -- VS Code with an XML extension - -**Tips for translators:** -- Keep translations concise (UI space is limited) -- Preserve any placeholders like `{0}`, `{1}` in the translation -- Test your translations by running the app locally if possible -- Feel free to submit partial translations—every bit helps! +**Supported languages**: English, German, Spanish, French, Italian, Japanese, Portuguese (Brazil), Russian, Chinese (Simplified), Arabic ### Improving Documentation diff --git a/CONTRIBUTING_TRANSLATIONS.md b/CONTRIBUTING_TRANSLATIONS.md new file mode 100644 index 00000000..f5c0d6b3 --- /dev/null +++ b/CONTRIBUTING_TRANSLATIONS.md @@ -0,0 +1,365 @@ +# Contributing Translations to Melodee + +Thank you for your interest in helping translate Melodee! Making the application accessible to users worldwide is a community effort, and we greatly appreciate your help. + +## Table of Contents + +- [Overview](#overview) +- [Supported Languages](#supported-languages) +- [Translation Status](#translation-status) +- [Getting Started](#getting-started) +- [How to Translate](#how-to-translate) +- [Translation Guidelines](#translation-guidelines) +- [Testing Your Translations](#testing-your-translations) +- [Submitting Translations](#submitting-translations) +- [For Developers: Adding New Keys](#for-developers-adding-new-keys) +- [FAQ](#faq) + +## Overview + +Melodee uses standard .NET resource files (`.resx`) for localization. These are XML files that contain key-value pairs where: +- **Key**: A unique identifier (e.g., `Actions.Save`, `Messages.WelcomeUser`) +- **Value**: The translated text for that language + +All resource files are located in: +``` +src/Melodee.Blazor/Resources/ +``` + +## Supported Languages + +| Language | Code | File | Status | RTL Support | +|----------|------|------|--------|-------------| +| English (US) | en-US | `SharedResources.resx` | ✅ 100% | No | +| Arabic (Saudi Arabia) | ar-SA | `SharedResources.ar-SA.resx` | 🔄 98% | Yes | +| Czech (Czechia) | cs-CZ | `SharedResources.cs-CZ.resx` | 🔄 6% | No | +| German | de-DE | `SharedResources.de-DE.resx` | 🔄 98% | No | +| Spanish | es-ES | `SharedResources.es-ES.resx` | 🔄 98% | No | +| Persian (Iran) | fa-IR | `SharedResources.fa-IR.resx` | 🔄 14% | Yes | +| French | fr-FR | `SharedResources.fr-FR.resx` | 🔄 98% | No | +| Indonesian (Indonesia) | id-ID | `SharedResources.id-ID.resx` | 🔄 6% | No | +| Italian | it-IT | `SharedResources.it-IT.resx` | 🔄 34% | No | +| Japanese | ja-JP | `SharedResources.ja-JP.resx` | 🔄 98% | No | +| Korean (Korea) | ko-KR | `SharedResources.ko-KR.resx` | 🔄 6% | No | +| Dutch (Netherlands) | nl-NL | `SharedResources.nl-NL.resx` | 🔄 6% | No | +| Polish (Poland) | pl-PL | `SharedResources.pl-PL.resx` | 🔄 6% | No | +| Portuguese (Brazil) | pt-BR | `SharedResources.pt-BR.resx` | 🔄 98% | No | +| Russian | ru-RU | `SharedResources.ru-RU.resx` | 🔄 98% | No | +| Swedish (Sweden) | sv-SE | `SharedResources.sv-SE.resx` | 🔄 6% | No | +| Turkish (Turkey) | tr-TR | `SharedResources.tr-TR.resx` | 🔄 6% | No | +| Ukrainian (Ukraine) | uk-UA | `SharedResources.uk-UA.resx` | 🔄 6% | No | +| Vietnamese (Vietnam) | vi-VN | `SharedResources.vi-VN.resx` | 🔄 6% | No | +| Chinese (Simplified) | zh-CN | `SharedResources.zh-CN.resx` | 🔄 98% | No | +## Translation Status + +Entries marked with `[NEEDS TRANSLATION]` require translation. You can find the current count of untranslated entries by searching for this marker in each file. + +To check status from the command line: +```bash +grep -c "NEEDS TRANSLATION" src/Melodee.Blazor/Resources/SharedResources.*.resx +``` + +**Want to add a new language?** Open an issue on GitHub to discuss adding support for your language. + +## Getting Started + +### Prerequisites + +- Git +- A text editor (VS Code, Notepad++, Sublime Text, etc.) +- Optionally: .NET 10 SDK to test your translations locally + +### Fork and Clone + +```bash +# Fork the repository on GitHub, then: +git clone https://github.com/YOUR_USERNAME/melodee.git +cd melodee + +# Create a branch for your translations +git checkout -b translations/your-language-code +``` + +## How to Translate + +### Step 1: Open Your Language File + +Navigate to `src/Melodee.Blazor/Resources/` and open the file for your language. + +For example, for Spanish: +``` +src/Melodee.Blazor/Resources/SharedResources.es-ES.resx +``` + +### Step 2: Find Entries Needing Translation + +Search for `[NEEDS TRANSLATION]` in the file. These entries look like: + +```xml + + [NEEDS TRANSLATION] Export + +``` + +### Step 3: Replace with Your Translation + +Remove the `[NEEDS TRANSLATION]` prefix and replace the entire value with your translation: + +```xml + + Exportar + +``` + +### Step 4: Preserve Placeholders + +Many strings contain placeholders like `{0}`, `{1}`, etc. **These must be preserved** in your translation: + +```xml + + + Found {0} items in {1} albums + + + + + Se encontraron {0} elementos en {1} álbumes + +``` + +### Step 5: Save and Validate + +After making changes, run the validation script: + +```bash +bash scripts/validate-resources.sh +``` + +This ensures all language files have consistent keys. + +## Translation Guidelines + +### Do's ✅ + +- **Keep translations concise** - UI space is often limited +- **Preserve placeholders** - `{0}`, `{1}`, etc. must remain in the translation +- **Match the tone** - Melodee uses a friendly, informal tone +- **Use native terms** - Translate technical terms appropriately for your locale +- **Be consistent** - Use the same translation for the same term throughout +- **Consider context** - The key name often hints at where the text appears (e.g., `Actions.` for buttons, `Messages.` for notifications) + +### Don'ts ❌ + +- **Don't translate placeholders** - Keep `{0}`, `{1}` as-is +- **Don't translate key names** - Only translate the `` content +- **Don't add HTML or formatting** - Keep translations as plain text +- **Don't use machine translation without review** - Always verify automated translations + +### Key Naming Convention + +Understanding key prefixes helps provide context: + +| Prefix | Usage | Example | +|--------|-------|---------| +| `Actions.` | Buttons and clickable actions | `Actions.Save`, `Actions.Delete` | +| `Common.` | Shared labels used in many places | `Common.Name`, `Common.Description` | +| `Messages.` | User notifications and feedback | `Messages.SaveSuccess`, `Messages.Error` | +| `Navigation.` | Menu items and navigation | `Navigation.Dashboard`, `Navigation.Settings` | +| `Validation.` | Form validation errors | `Validation.Required`, `Validation.InvalidEmail` | +| `Dialog.` | Dialog titles and content | `Dialog.ConfirmDelete`, `Dialog.Cancel` | +| `Tooltip.` | Hover tooltips | `Tooltip.ClickToEdit` | +| `{PageName}.` | Page-specific text | `Dashboard.RecentActivity`, `Albums.FilterByGenre` | + +### Examples of Good Translations + +**Simple button text:** +```xml + +Save + + +Guardar +``` + +**Text with placeholder:** +```xml + +Welcome back, {0}! + + +Bon retour, {0} ! +``` + +**Longer message:** +```xml + +Are you sure you want to delete this album? This action cannot be undone. + + +Möchten Sie dieses Album wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. +``` + +## Testing Your Translations + +### Running Locally + +If you have .NET 10 SDK installed: + +```bash +# Build and run +dotnet run --project src/Melodee.Blazor + +# Open http://localhost:5157 in your browser +# Change language using the language selector in the header +``` + +### Visual Inspection + +After running, check: +- Text doesn't overflow containers +- Buttons and labels are readable +- Numbers and dates format correctly +- RTL layout works (for Arabic) + +## Submitting Translations + +### 1. Validate Your Changes + +```bash +bash scripts/validate-resources.sh +``` + +All checks must pass before submitting. + +### 2. Commit Your Changes + +```bash +git add src/Melodee.Blazor/Resources/SharedResources.YOUR-LANG.resx +git commit -m "translations(YOUR-LANG): translate X entries" +``` + +**Commit message examples:** +- `translations(es-ES): translate 50 action labels` +- `translations(ja-JP): complete settings page translations` +- `translations(de-DE): fix typo in welcome message` + +### 3. Push and Create Pull Request + +```bash +git push origin translations/your-language-code +``` + +Then create a Pull Request on GitHub with: +- Title: `translations(LANG): brief description` +- Description: List what you translated +- Any notes about translation choices + +### 4. Review Process + +A maintainer will review your translations. For languages maintainers don't speak, we may: +- Ask community members to verify +- Use review tools to check consistency +- Ask clarifying questions + +## For Developers: Adding New Keys + +**⚠️ CRITICAL**: When adding ANY new localization key, you **MUST** add it to ALL 20 language files in a SINGLE commit. + +### Use the Helper Script (Recommended) + +```bash +bash scripts/add-localization-key.sh "YourKey.Name" "English text here" +``` + +This automatically: +- Adds the key to all 20 resource files +- Uses the English value for en-US +- Adds `[NEEDS TRANSLATION]` prefix for other languages + +### Manual Addition + +If adding manually, you must update ALL files: +1. `SharedResources.resx` (English - base file) +2. `SharedResources.ar-SA.resx` +3. `SharedResources.cs-CZ.resx` +4. `SharedResources.de-DE.resx` +5. `SharedResources.es-ES.resx` +6. `SharedResources.fa-IR.resx` +7. `SharedResources.fr-FR.resx` +8. `SharedResources.id-ID.resx` +9. `SharedResources.it-IT.resx` +10. `SharedResources.ja-JP.resx` +11. `SharedResources.ko-KR.resx` +12. `SharedResources.nl-NL.resx` +13. `SharedResources.pl-PL.resx` +14. `SharedResources.pt-BR.resx` +15. `SharedResources.ru-RU.resx` +16. `SharedResources.sv-SE.resx` +17. `SharedResources.tr-TR.resx` +18. `SharedResources.uk-UA.resx` +19. `SharedResources.vi-VN.resx` +20. `SharedResources.zh-CN.resx` + +### Validation + +**Always run validation before committing:** +```bash +bash scripts/validate-resources.sh +``` + +The CI/CD pipeline will fail if keys are missing from any language file. + +## FAQ + +### Can I submit partial translations? + +**Yes!** Every contribution helps. You don't need to translate everything at once. + +### What if I'm unsure about a translation? + +Add a comment in your PR explaining the uncertainty. The community can help review. + +### Can I improve existing translations? + +Absolutely! If you see a better way to phrase something, submit a PR with improvements. + +### What if a placeholder format changes in different languages? + +Keep the placeholders (`{0}`, `{1}`) but feel free to reorder them if your language requires a different word order. The numbers correspond to specific values, not positions. + +### How do I handle plural forms? + +Currently, Melodee uses simple placeholder-based translations. For complex plural rules, we use separate keys like `Messages.OneItem` and `Messages.MultipleItems`. + +### Can I use regional variations? + +For now, we use one file per language (e.g., `es-ES` for all Spanish). Regional variations can be discussed via GitHub issues. + +### What tools can I use to edit .resx files? + +- **Text editors**: VS Code, Sublime Text, Notepad++ (files are XML) +- **Visual Studio**: Built-in resource editor with a nice GUI +- **ResX Resource Manager**: Free VS extension for managing multiple resource files +- **Online**: XML editors or specialized .resx editors + +### The validation script found missing keys. What do I do? + +If you're only translating (not adding new keys), this shouldn't happen. If it does: +1. Check if someone else added keys recently +2. Pull the latest changes from main +3. Contact maintainers if the issue persists + +## Recognition + +Translation contributors are recognized in: +- Release notes when translations are added/improved +- The GitHub contributors page +- Special thanks in major releases + +## Questions? + +- **GitHub Discussions**: [Ask questions](https://github.com/melodee-project/melodee/discussions) +- **Discord**: [Join our community](https://discord.gg/bfMnEUrvbp) +- **Issues**: [Report problems](https://github.com/melodee-project/melodee/issues) + +Thank you for helping make Melodee accessible to users worldwide! 🌍🎵 diff --git a/Directory.Packages.props b/Directory.Packages.props index 0d9383d3..326ac973 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,7 +86,7 @@ - + @@ -112,6 +112,7 @@ + @@ -128,6 +129,7 @@ + diff --git a/README.md b/README.md index 9504e2f5..2dad1e8a 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,13 @@ Experience Melodee before installing! Our official demo server is available at: - **⚡ Automatic Ingestion**: Drop files → play music (validated albums flow through automatically) - **🔄 Automated Jobs**: Cron-based scheduling with intelligent job chaining - **📝 User Requests**: Submit and track requests for missing albums/songs, with automatic completion when matches are detected +- **🎙️ Podcast Support**: Subscribe to podcasts, auto-download episodes, playback tracking with resume positions - **🎵 OpenSubsonic API**: Compatible with popular Subsonic and OpenSubsonic clients - **🎬 Jellyfin API**: Compatible with Jellyfin music clients (Finamp, Feishin, Streamyfin) - **🌐 Melodee API**: Fast RESTful API for custom integrations - **🌐 Modern Web UI**: Blazor Server interface with Radzen UI components +- **🎛️ Jukebox**: Server-side playback with queue/control support (OpenSubsonic jukeboxControl; MPV/MPD backends) +- **🎉 Party Mode**: Shared listening sessions with a collaborative queue and DJ/Listener roles - **🐳 Container Ready**: Full Docker/Podman support with PostgreSQL ![Melodee Web Interface](graphics/Snapshot_2025-02-04_23-06-24.png) @@ -292,6 +295,10 @@ Melodee includes a comprehensive job scheduling system powered by Quartz.NET: | **ChartUpdateJob** | Links chart entries to albums | Daily at 2 AM | | **MusicBrainzUpdateDatabaseJob** | Updates local MusicBrainz cache | Monthly | | **NowPlayingCleanupJob** | Cleans stale now-playing entries | Every 5 minutes | +| **PodcastRefreshJob** | Fetches new episodes from subscribed podcasts | Every 30 minutes | +| **PodcastDownloadJob** | Downloads queued podcast episodes | Every 5 minutes | +| **PodcastCleanupJob** | Enforces retention policies on downloaded episodes | Daily at 3 AM | +| **PodcastRecoveryJob** | Resets stuck downloads and cleans orphaned files | Every hour | Jobs can be manually triggered, paused, or monitored from the admin UI at `/admin/jobs`. @@ -333,15 +340,15 @@ Melodee features comprehensive localization with support for 10 languages: | Language | Code | Status | RTL | |----------|------|--------|-----| | English (US) | en-US | ✅ 100% | - | -| Arabic | ar-SA | 🔄 31% | ✅ | -| Chinese (Simplified) | zh-CN | 🔄 38% | - | -| French | fr-FR | 🔄 37% | - | -| German | de-DE | 🔄 41% | - | -| Italian | it-IT | 🔄 41% | - | -| Japanese | ja-JP | 🔄 37% | - | -| Portuguese (Brazil) | pt-BR | 🔄 42% | - | -| Russian | ru-RU | 🔄 38% | - | -| Spanish | es-ES | 🔄 38% | - | +| Arabic | ar-SA | 🔄 29% | ✅ | +| Chinese (Simplified) | zh-CN | 🔄 35% | - | +| French | fr-FR | 🔄 34% | - | +| German | de-DE | 🔄 39% | - | +| Italian | it-IT | 🔄 38% | - | +| Japanese | ja-JP | 🔄 34% | - | +| Portuguese (Brazil) | pt-BR | 🔄 39% | - | +| Russian | ru-RU | 🔄 35% | - | +| Spanish | es-ES | 🔄 35% | - | - **Language Selector**: Available in the header for quick switching - **User Preference**: Language choice is saved to your user profile and persists across sessions @@ -360,7 +367,7 @@ We welcome translation contributions from the community! Strings marked with `[N Resource files are standard .NET `.resx` XML format. You can edit them with any text editor or Visual Studio's resource editor. -**Current translation needs:** ~850-1000 strings per language need native translations. See [Contributing Guide](CONTRIBUTING.md) for details. +**Current translation needs:** ~965-1,123 strings per language need native translations (out of 1,577 total). See [Contributing Guide](CONTRIBUTING.md) for details. ### 🌐 OpenSubsonic API diff --git a/benchmarks/Melodee.Benchmarks/CacheBenchmarks.cs b/benchmarks/Melodee.Benchmarks/CacheBenchmarks.cs index 6a7844d7..01e51601 100644 --- a/benchmarks/Melodee.Benchmarks/CacheBenchmarks.cs +++ b/benchmarks/Melodee.Benchmarks/CacheBenchmarks.cs @@ -9,7 +9,7 @@ namespace Melodee.Benchmarks; /// -/// Benchmarks for caching operations addressing PERFORMANCE_REVIEW.md requirements +/// Benchmarks for caching operations addressing caching performance requirements /// [SimpleJob(RuntimeMoniker.HostProcess)] [MemoryDiagnoser] diff --git a/benchmarks/Melodee.Benchmarks/CollectionOperationBenchmarks.cs b/benchmarks/Melodee.Benchmarks/CollectionOperationBenchmarks.cs index f7631cc1..6e599454 100644 --- a/benchmarks/Melodee.Benchmarks/CollectionOperationBenchmarks.cs +++ b/benchmarks/Melodee.Benchmarks/CollectionOperationBenchmarks.cs @@ -5,7 +5,7 @@ namespace Melodee.Benchmarks; /// -/// Benchmarks for collection operations addressing PERFORMANCE_REVIEW.md requirements +/// Benchmarks for collection operations addressing collection performance requirements /// [SimpleJob(RuntimeMoniker.HostProcess)] [MemoryDiagnoser] diff --git a/benchmarks/Melodee.Benchmarks/DEMO.md b/benchmarks/Melodee.Benchmarks/DEMO.md index c30ebee0..65a37a30 100644 --- a/benchmarks/Melodee.Benchmarks/DEMO.md +++ b/benchmarks/Melodee.Benchmarks/DEMO.md @@ -29,8 +29,8 @@ dotnet run -c Release --project benchmarks/Melodee.Benchmarks collection ## Build Status: ✅ WORKING The benchmarks build successfully and execute properly, covering all the performance concerns identified in both: -- `API_REVIEW_FIX.md` (streaming performance) -- `PERFORMANCE_REVIEW.md` (general performance issues) +- Streaming performance requirements +- General performance issues ## Available Benchmark Categories diff --git a/benchmarks/Melodee.Benchmarks/DatabaseQueryBenchmarks.cs b/benchmarks/Melodee.Benchmarks/DatabaseQueryBenchmarks.cs index f6614c97..511a5406 100644 --- a/benchmarks/Melodee.Benchmarks/DatabaseQueryBenchmarks.cs +++ b/benchmarks/Melodee.Benchmarks/DatabaseQueryBenchmarks.cs @@ -12,7 +12,7 @@ namespace Melodee.Benchmarks; /// -/// Benchmarks for database query operations addressing PERFORMANCE_REVIEW.md requirements +/// Benchmarks for database query operations addressing database performance requirements /// [SimpleJob(RuntimeMoniker.HostProcess)] [MemoryDiagnoser] diff --git a/benchmarks/Melodee.Benchmarks/README.md b/benchmarks/Melodee.Benchmarks/README.md index ff35caa7..ea4ff1c8 100644 --- a/benchmarks/Melodee.Benchmarks/README.md +++ b/benchmarks/Melodee.Benchmarks/README.md @@ -45,7 +45,7 @@ dotnet run -c Release --project benchmarks/Melodee.Benchmarks all -- --exporters ## Benchmark Categories ### 1. Streaming Benchmarks (`streaming`) -**Addresses**: API_REVIEW_FIX.md streaming performance requirements +**Addresses**: streaming performance requirements - **File Streaming Performance**: Tests different buffer sizes and streaming approaches - **Range Request Processing**: Benchmarks HTTP range header parsing and processing @@ -58,7 +58,7 @@ dotnet run -c Release --project benchmarks/Melodee.Benchmarks all -- --exporters - Buffer size optimization (4KB to 256KB tested) ### 2. Database Query Benchmarks (`database`) -**Addresses**: PERFORMANCE_REVIEW.md database performance concerns +**Addresses**: database performance concerns - **Complex Query Patterns**: Tests nested Include().ThenInclude() chains - **Pagination Performance**: Compares paginated vs unbounded queries @@ -71,7 +71,7 @@ dotnet run -c Release --project benchmarks/Melodee.Benchmarks all -- --exporters - Database connection pool efficiency ### 3. Cache Benchmarks (`cache`) -**Addresses**: PERFORMANCE_REVIEW.md caching concerns +**Addresses**: caching concerns - **Cache Hit/Miss Ratios**: Measures cache effectiveness under load - **Eviction Policies**: Tests LRU vs time-based eviction strategies @@ -84,7 +84,7 @@ dotnet run -c Release --project benchmarks/Melodee.Benchmarks all -- --exporters - Concurrent access performance ### 4. Collection Operation Benchmarks (`collection`) -**Addresses**: PERFORMANCE_REVIEW.md collection efficiency concerns +**Addresses**: collection efficiency concerns - **LINQ Optimization**: Tests multiple ToList() calls vs optimized chains - **Playlist Reordering**: Benchmarks the inefficient PlaylistService patterns @@ -210,5 +210,3 @@ benchmarks/artifacts/ ## References - [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/) -- [API_REVIEW_FIX.md](../../prompts/API_REVIEW_FIX.md) - Streaming performance requirements -- [PERFORMANCE_REVIEW.md](../../prompts/PERFORMANCE_REVIEW.md) - General performance concerns diff --git a/benchmarks/Melodee.Benchmarks/StreamingBenchmarks.cs b/benchmarks/Melodee.Benchmarks/StreamingBenchmarks.cs index a230b8ad..d4285ee4 100644 --- a/benchmarks/Melodee.Benchmarks/StreamingBenchmarks.cs +++ b/benchmarks/Melodee.Benchmarks/StreamingBenchmarks.cs @@ -5,7 +5,7 @@ namespace Melodee.Benchmarks; /// -/// Benchmarks for streaming operations addressing API_REVIEW_FIX.md requirements +/// Benchmarks for streaming operations addressing streaming performance requirements /// [SimpleJob(RuntimeMoniker.HostProcess)] [MemoryDiagnoser] diff --git a/design/README.md b/design/README.md new file mode 100644 index 00000000..4e0fbc85 --- /dev/null +++ b/design/README.md @@ -0,0 +1,54 @@ +# Melodee Design Documentation + +This directory contains internal design documentation, implementation notes, testing guides, and other technical documents that are **NOT** part of the public Jekyll documentation site. + +## Directory Structure + +| Directory | Purpose | +|-----------|---------| +| `adr/` | Architecture Decision Records - documenting significant architectural decisions | +| `backlog/` | Feature backlog and planning documents | +| `charts/` | Chart data files (JSON) for importing music charts | +| `docs/` | Implementation documentation, session summaries, fix documentation | +| `requirements/` | Feature requirements and specifications | +| `runbooks/` | Operational runbooks for debugging and maintenance | +| `testing/` | Manual test guides, test coverage reports, testing documentation | + +## Important Note for AI Agents + +**DO NOT create documentation files in the `/docs` directory.** + +The `/docs` directory is exclusively for the Jekyll-based GitHub Pages documentation site. Files placed there will be: +- Published to the public documentation website +- Subject to Jekyll processing and formatting requirements +- Visible to end users + +Instead, place internal documentation in the appropriate subdirectory here in `/design`: + +| Document Type | Location | +|---------------|----------| +| Implementation notes | `design/docs/` | +| Session summaries | `design/docs/` | +| Fix documentation | `design/docs/` | +| Requirements specs | `design/requirements/` | +| Testing guides | `design/testing/` | +| Test reports | `design/testing/` | +| Runbooks | `design/runbooks/` | +| ADRs | `design/adr/` | + +## Public Documentation + +To add or modify **public-facing documentation** (user guides, API docs, etc.), edit files in: +- `/docs/pages/` - Main documentation pages +- `/docs/_data/toc.yml` - Table of contents navigation + +Public documentation uses Jekyll with the following front matter format: + +```yaml +--- +title: Page Title +description: Brief description +tags: + - relevant-tag +--- +``` diff --git a/design/adr/ADR-0001-do-not-implement-opensubsonic-subsonic-jukebox-control.md b/design/adr/ADR-0001-do-not-implement-opensubsonic-subsonic-jukebox-control.md new file mode 100644 index 00000000..80265f1a --- /dev/null +++ b/design/adr/ADR-0001-do-not-implement-opensubsonic-subsonic-jukebox-control.md @@ -0,0 +1,36 @@ +## ADR-0001: Do not implement OpenSubsonic/Subsonic Jukebox Control + +- Date: 2025-12-13T16:17:46.094Z +- Status: Superseded (see ADR-0007) + +### Context + +OpenSubsonic/Subsonic defines a `jukeboxControl` endpoint intended for **server-side playback control** (“jukebox mode”), where the server is responsible for maintaining a playback queue and emitting audio through an audio output accessible to the server process. + +Melodee is commonly deployed as a web application in **headless** server environments (e.g., Proxmox) and often within **containers**. + +### Decision + +We will **not implement** server-side jukebox playback. + +Instead, Melodee will implement the `jukeboxControl` endpoint(s) but always respond with **HTTP 410 Gone** ("not supported") to indicate the feature is intentionally unavailable. + +### Rationale + +- Typical Melodee deployments (including the author’s) run in Proxmox/containerized environments where: + - There is no reliable, configured audio output device exposed to the application. + - There is no long-running audio playback engine/process integrated with Melodee. +- Implementing jukebox properly would require additional OS/device integration and persistent playback state management that does not align with Melodee’s primary usage (streaming to clients). + +### Consequences + +- Clients that attempt to use jukebox control will receive a clear, consistent "not supported" response. +- Melodee remains focused on streaming playback to clients rather than acting as a local player. + +### Revisit / Future Work + +If a contributor wants to add jukebox support in the future, it should likely be implemented as an optional plugin/module with explicit documentation for: +- required audio backend (ALSA/Pulse/etc.) +- required container/VM device pass-through +- state persistence and concurrency semantics + diff --git a/design/adr/ADR-0002-similar-songs-is-admin-managed-artistrelationtype-similar.md b/design/adr/ADR-0002-similar-songs-is-admin-managed-artistrelationtype-similar.md new file mode 100644 index 00000000..5705cc54 --- /dev/null +++ b/design/adr/ADR-0002-similar-songs-is-admin-managed-artistrelationtype-similar.md @@ -0,0 +1,35 @@ +## ADR-0002: Similar Songs is admin-managed (ArtistRelationType.Similar) + +- Date: 2025-12-13T16:30:20.883Z +- Status: Accepted + +### Context + +OpenSubsonic defines `getSimilarSongs` / `getSimilarSongs2`. Many servers implement this by calling third-party services (Last.fm/Spotify/etc.) or by doing behavior-based recommendations. + +Melodee already has an `ArtistRelation` table and an `ArtistRelationType.Similar` value, and Melodee supports role-based editing (Admin/Editor). + +### Decision + +Melodee will compute “similar songs” using **curated, local similarity**: + +- Similarity is defined by **Artist → Similar Artists** relationships managed by Admin/Editor users (`ArtistRelationType.Similar`). +- `getSimilarSongs(2)` will return songs drawn from: + 1) the requested song/artist’s own catalog (optional), and + 2) the catalog of related “similar” artists. + +### Rationale + +- Avoids reliance on third-party APIs/credentials and improves determinism. +- Fits a self-hosted/air-gapped deployment model. +- Allows the library owner to control recommendations and quality. + +### Consequences + +- Similar songs quality depends on how well similarity relationships are maintained. +- Requires UI/management workflows for Admin/Editor to curate “similar” relationships. + +### Revisit / Future Work + +Optionally add fallback strategies when there are no curated relationships (e.g., same genre/tags, same contributors, play-history co-occurrence) but keep curated similarity as the primary signal. + diff --git a/design/adr/ADR-0003-external-integrations-use-settings-caching.md b/design/adr/ADR-0003-external-integrations-use-settings-caching.md new file mode 100644 index 00000000..6d4c88eb --- /dev/null +++ b/design/adr/ADR-0003-external-integrations-use-settings-caching.md @@ -0,0 +1,29 @@ +## ADR-0003: External integrations use Settings + caching + +- Date: 2025-12-13T16:35:42.249Z +- Status: Accepted + +### Context + +Melodee integrates with external services (Spotify, Last.fm, iTunes, etc.) for search/scrobbling/metadata. + +We need a consistent strategy for where credentials are stored, what happens when credentials are missing/invalid, and how to limit repetitive API calls. + +### Decision + +- All external API keys/secrets/tokens are stored in the **Settings** table. +- If credentials are **missing or invalid**, the integration is considered **disabled**. + - The system should **fail gracefully** (no unhandled exceptions) and return an empty/neutral result. +- External API calls should be cached using the DI-injected `ICacheManager`. + +### Rationale + +- Centralizes configuration for self-hosted deployments. +- Allows explicit enable/disable behavior without depending on environment variables. +- Reduces rate-limit pressure and improves UI responsiveness. + +### Consequences + +- Some integrations require UI/admin workflows to populate settings. +- Caching introduces staleness; cache keys and TTLs must be chosen carefully. + diff --git a/design/adr/ADR-0004-last-fm-session-key-lifecycle-per-user.md b/design/adr/ADR-0004-last-fm-session-key-lifecycle-per-user.md new file mode 100644 index 00000000..fb5c79f2 --- /dev/null +++ b/design/adr/ADR-0004-last-fm-session-key-lifecycle-per-user.md @@ -0,0 +1,33 @@ +## ADR-0004: Last.fm session key lifecycle (per-user) + +- Date: 2025-12-13T16:38:33.838Z +- Status: Accepted + +### Context + +Last.fm scrobbling requires a user-authorized **session key** (`sk`). Session keys typically do not expire, but can be revoked by the user in Last.fm, and any invalidation must be handled gracefully. + +Melodee is a server-hosted app; we should not ask users to provide their Last.fm password to Melodee. + +### Decision + +- Use the **web authentication** flow (`auth.getSession`) to obtain a session key. + - Do **not** use `auth.getMobileSession` (requires collecting user password). +- Store the session key **per Melodee user** in the database (`User.LastFmSessionKey`). + - Treat as a secret: never log it and do not expose it via APIs. +- Runtime behavior: + - If global Last.fm scrobbling is enabled but the user has no session key: return success/no-op (and optionally log once at Debug/Warn). + - If Last.fm returns an "invalid session" error during scrobble/now-playing: clear `User.LastFmSessionKey` and require re-linking. +- There is no refresh flow; re-authentication is the only recovery after revocation. + +### Rationale + +- Avoids handling user passwords and matches Last.fm’s recommended OAuth-style flow. +- Keeps scrobbling user-scoped (different Melodee users can link different Last.fm accounts). +- Makes revocation safe and explicit. + +### Consequences + +- Requires a UI flow to link/unlink Last.fm for a user. +- Some failures will look like silent no-ops (by design) unless surfaced in UI. + diff --git a/design/adr/ADR-0005-jellyfin-desktop-and-web-ui-dependent-clients-are-not-compatible-with-melodee.md b/design/adr/ADR-0005-jellyfin-desktop-and-web-ui-dependent-clients-are-not-compatible-with-melodee.md new file mode 100644 index 00000000..6ee74acf --- /dev/null +++ b/design/adr/ADR-0005-jellyfin-desktop-and-web-ui-dependent-clients-are-not-compatible-with-melodee.md @@ -0,0 +1,62 @@ +## ADR-0005: Jellyfin Desktop and Web UI-dependent clients are not compatible with Melodee + +- Date: 2026-01-02T15:36:00.000Z +- Status: Accepted + +### Context + +During Jellyfin API compatibility testing, we evaluated multiple Jellyfin client applications for use with Melodee's Jellyfin API implementation. Testing revealed a fundamental architectural distinction between client types. + +**Jellyfin Desktop** (and similar clients like Jellyfin Media Player, official Jellyfin mobile apps) are Qt/Electron wrappers around the **Jellyfin Web UI**. These clients: +1. Connect to a server and verify connectivity via `/System/Info/Public` +2. Load the full Jellyfin Web UI from the server URL +3. Inject native player plugins (e.g., mpv) for enhanced playback +4. All user interaction happens through the web interface + +**Pure API clients** like Gelly, Finamp, Feishin, and Symfonium: +1. Implement their own native UI +2. Communicate exclusively via the Jellyfin REST API +3. Do not require the server to host any web assets + +### Decision + +Melodee will **not attempt to serve the Jellyfin Web UI** or support web UI-dependent clients. + +Melodee's Jellyfin API implementation targets **pure API clients only**. + +### Rationale + +- Melodee is a music server, not a Jellyfin server fork. Hosting the full Jellyfin Web UI would require: + - Bundling/serving significant static web assets (~50MB+) + - Maintaining compatibility with Jellyfin Web UI updates + - Supporting video/TV/movie UI elements irrelevant to a music-only server +- Pure API clients (Gelly, Finamp, etc.) provide excellent user experiences for music playback without this overhead +- The Jellyfin API is well-documented and sufficient for music streaming use cases + +### Compatible Clients (Tested/Recommended) + +| Client | Platform | Status | Notes | +|--------|----------|--------|-------| +| Gelly | Linux (GTK) | ✅ Tested | Full functionality confirmed | +| Finamp | iOS/Android | 🎯 Target | Pure API client for music | +| Feishin | Desktop | 🎯 Target | Cross-platform music player | +| Symfonium | Android | 🎯 Target | Popular music streaming app | + +### Incompatible Clients + +| Client | Reason | +|--------|--------| +| Jellyfin Desktop | Requires Jellyfin Web UI | +| Jellyfin Media Player | Requires Jellyfin Web UI | +| Official Jellyfin Apps | Require Jellyfin Web UI | + +### Consequences + +- Users expecting to use Jellyfin Desktop with Melodee will see a blank screen after connection +- Documentation should clearly list compatible vs incompatible clients +- API development focuses on endpoints used by pure API clients + +### Revisit / Future Work + +If significant user demand exists, a minimal "music-only" web UI could be considered as a separate project/plugin, but this is not planned. + diff --git a/design/adr/ADR-0006-mobile-jellyfin-clients-finamp-streamyfin-require-dedicated-build-environments.md b/design/adr/ADR-0006-mobile-jellyfin-clients-finamp-streamyfin-require-dedicated-build-environments.md new file mode 100644 index 00000000..b3c3616a --- /dev/null +++ b/design/adr/ADR-0006-mobile-jellyfin-clients-finamp-streamyfin-require-dedicated-build-environments.md @@ -0,0 +1,56 @@ +## ADR-0006: Mobile Jellyfin clients (Finamp, Streamyfin) require dedicated build environments + +- Date: 2026-01-02T16:05:00.000Z +- Status: Accepted + +### Context + +During Jellyfin API compatibility testing, we evaluated mobile-focused Jellyfin clients Finamp and Streamyfin for testing against Melodee's Jellyfin API implementation. + +**Finamp** (Flutter/Dart): +- Cross-platform mobile app targeting iOS and Android +- Requires Flutter SDK, Android SDK, Xcode (for iOS), and platform-specific toolchains +- Cannot be easily run on a Linux desktop without Android emulator or physical device + +**Streamyfin** (React Native/Expo): +- Mobile app using Expo framework +- Requires Expo CLI, Node.js, and either iOS Simulator (macOS only) or Android emulator +- `npx expo start` launches a development server but requires mobile device/emulator to render + +### Decision + +Melodee will **not maintain local build/test environments** for mobile Jellyfin clients during development. + +Instead, API compatibility will be validated through: +1. **API-level testing** via shell scripts (e.g., `test-jellyfin-api.sh`) that exercise all endpoints +2. **Desktop pure-API clients** like Gelly for interactive testing +3. **Community feedback** from users running mobile clients against Melodee + +### Rationale + +- Setting up Flutter/Android SDK or React Native/Expo with emulators is significant overhead for a .NET server project +- API-level testing provides equivalent coverage for server-side compatibility +- Desktop clients like Gelly use the same Jellyfin API endpoints as mobile clients +- Mobile-specific issues (if any) are more likely UI/UX bugs in the client than API incompatibilities + +### Consequences + +- Mobile client compatibility is validated indirectly through API tests +- Bugs specific to mobile clients may only surface via community reports +- Documentation should encourage community testing with mobile clients + +### Compatible Mobile Clients (Target) + +| Client | Platform | API Compatibility | +|--------|----------|-------------------| +| Finamp | iOS/Android | Expected compatible (same API as Gelly) | +| Streamyfin | iOS/Android | Expected compatible (uses @jellyfin/sdk) | +| Symfonium | Android | Expected compatible (pure API client) | + +### Testing Strategy + +1. Maintain comprehensive `test-jellyfin-api.sh` covering all endpoints used by mobile clients +2. Use Gelly (desktop) for interactive/manual testing during development +3. Document API endpoints and expected responses for community verification +4. Address mobile-specific issues as they are reported + diff --git a/design/adr/ADR-0007-party-mode-shared-sessions-is-the-primary-jukebox-feature-server-side-playback-is-optional.md b/design/adr/ADR-0007-party-mode-shared-sessions-is-the-primary-jukebox-feature-server-side-playback-is-optional.md new file mode 100644 index 00000000..9efdc821 --- /dev/null +++ b/design/adr/ADR-0007-party-mode-shared-sessions-is-the-primary-jukebox-feature-server-side-playback-is-optional.md @@ -0,0 +1,30 @@ +## ADR-0007: Party mode (shared sessions) is the primary "jukebox" feature; server-side playback is optional + +- Date: 2026-01-09T00:00:00.000Z +- Status: Accepted + +### Context + +Melodee users commonly ask for “jukebox” / “party mode”: a shared queue that multiple users can control, with music playing on a designated endpoint (e.g., a browser tab on a TV). + +The Subsonic/OpenSubsonic `jukeboxControl` endpoint implies server-side playback and is often incompatible with headless/container deployments unless explicitly configured. + +### Decision + +- The core feature is **Party Mode**: shared sessions + shared queue + real-time updates, with playback happening on a designated endpoint (initially the existing Blazor `/musicplayer`). +- OpenSubsonic/Subsonic `jukeboxControl` remains **disabled by default** (HTTP `410 Gone`). +- If a deployment explicitly configures a jukebox backend (Snapcast/MPD/etc.), Melodee may enable `jukeboxControl` semantics scoped to that backend. + +### Rationale + +- Delivers the competitive “jukebox” experience without forcing server-side audio output in the default product. +- Keeps behavior explicit and safe for self-hosted deployments. + +### Consequences + +- Party mode requires a first-class data model (sessions, participants, queue, playback state) and a real-time update mechanism (SignalR preferred). +- `jukeboxControl` support becomes a compatibility layer, not the primary contract. + +### References + +- `design/requirements/jukebox-requirements.md` diff --git a/design/adr/README.md b/design/adr/README.md new file mode 100644 index 00000000..29999539 --- /dev/null +++ b/design/adr/README.md @@ -0,0 +1,58 @@ +## ADRs (Architecture Decision Records) + +This directory contains Melodee’s Architecture Decision Records (ADRs): short, durable documents that capture **why** a significant architectural or product decision was made. + +### When to write an ADR + +Write an ADR when a change is: + +- A meaningful product/architecture decision (not just an implementation detail) +- Hard to reverse or likely to be debated later +- Expected to influence multiple parts of the system or future roadmap + +### ADR lifecycle + +- **Accepted**: the decision is in effect. +- **Superseded**: a newer ADR replaces this one; do not delete history. +- **Deprecated**: no longer recommended, but not necessarily replaced. + +If a decision changes, prefer creating a **new ADR** and marking the old one **Superseded**. + +### File naming + +Use one ADR per file. + +**Casing preference:** Prefer readable kebab-case for filenames and avoid ALL-CAPS filenames (harder to scan in listings). + +- `ADR-0001-short-title.md` + +### ADR template + +Copy/paste and fill out: + +```markdown +## ADR-000X: + +- Date: 2026-01-09T04:13:52.861Z +- Status: Proposed | Accepted | Superseded | Deprecated + +### Context + +What problem are we solving? What constraints exist? What alternatives were considered? + +### Decision + +What did we decide to do? + +### Rationale + +Why is this the best choice right now? + +### Consequences + +What trade-offs does this introduce? What follow-up work is required? + +### References + +- Links to requirements docs, PRs, issues, or related ADRs +``` diff --git a/prompts/20260105-BACKLOG-IDEAS.md b/design/backlog/20260105-backlog-ideas.md similarity index 97% rename from prompts/20260105-BACKLOG-IDEAS.md rename to design/backlog/20260105-backlog-ideas.md index eaa8c5e0..b096be44 100644 --- a/prompts/20260105-BACKLOG-IDEAS.md +++ b/design/backlog/20260105-backlog-ideas.md @@ -46,7 +46,7 @@ - [6.4 Wear OS / Watch Support](#64-wear-os--watch-support) - [6.5 Desktop Widget / Menubar App](#65-desktop-widget--menubar-app) 8. [Integration & Ecosystem](#7-integration--ecosystem) - - [7.1 Podcast Support](#71-podcast-support) + - [7.1 Podcast Support ✅](#71-podcast-support-) - [7.2 Audiobook Support](#72-audiobook-support) - [7.3 Home Assistant Integration](#73-home-assistant-integration) - [7.4 Discord Rich Presence](#74-discord-rich-presence) @@ -971,32 +971,32 @@ Medium - Quality-of-life improvement for desktop users ## 7. Integration & Ecosystem -### 7.1 Podcast Support +### 7.1 Podcast Support ✅ COMPLETED + +**Status**: Fully implemented as of January 2026. **Problem Statement** Users want one app for all audio content. Currently they need a separate podcast app. -**Proposed Solution** -Native podcast support: -- **RSS feed subscription**: Add podcasts by URL -- **Directory integration**: Search Apple Podcasts, Spotify catalogs -- **Episode management**: Auto-download, played tracking -- **Variable speed playback**: 1x, 1.5x, 2x speeds -- **Smart resume**: Remember position in long episodes -- **Separate podcast library**: Don't mix with music - -**Technical Considerations** -- RSS parser for podcast feeds -- Episode storage (stream or download) -- Different playback controls (skip 30s, speed) -- Podcast-specific now playing UI +**What Was Delivered** +Full podcast support including: +- ✅ **RSS feed subscription**: Add podcasts by URL with automatic refresh +- ✅ **Directory integration**: iTunes podcast directory search and discovery +- ✅ **Episode management**: Auto-download with per-channel settings, played tracking +- ✅ **Per-channel settings**: Custom refresh intervals, max downloaded episodes, storage limits +- ✅ **Episode search**: Search across all episodes in subscribed podcasts +- ✅ **OPML import/export**: Migrate subscriptions to/from other podcast apps +- ✅ **Smart resume**: Automatic bookmarking, resume from last position +- ✅ **Dashboard pinning**: Pin favorite podcasts for quick access +- ✅ **OpenSubsonic API**: Full podcast endpoint support for compatible clients +- ✅ **Background jobs**: PodcastRefreshJob, PodcastDownloadJob, PodcastCleanupJob, PodcastRecoveryJob **User Impact** Medium-High - Broadens appeal, single solution for audio -**Priority**: 🟠 Medium -**Effort**: Large (4-6 weeks) -**Dependencies**: New subsystem, podcast APIs +**Priority**: ✅ COMPLETED +**Effort**: Large (4-6 weeks) - Delivered +**Dependencies**: Implemented --- @@ -1581,7 +1581,7 @@ Based on user impact and effort, here's a recommended prioritization: | Lyrics Integration | Medium-High | Medium | Popular request | | Cross-Device Handoff | High | Large | Premium feel | | Two-Factor Authentication | Medium | Medium | Security | -| Podcast Support | Medium-High | Large | Scope expansion | +| ~~Podcast Support~~ | ✅ | COMPLETED | Implemented January 2026 | | Activity Feed | Medium | Medium | Social feature | ### Tier 4: Lower Priority (Future Consideration) diff --git a/prompts/ONBOARDING-BACKLOG.md b/design/backlog/onboarding-backlog.md similarity index 100% rename from prompts/ONBOARDING-BACKLOG.md rename to design/backlog/onboarding-backlog.md diff --git a/docs/charts/1001_albums_you_must_hear_before_you_die.json b/design/charts/1001_albums_you_must_hear_before_you_die.json similarity index 100% rename from docs/charts/1001_albums_you_must_hear_before_you_die.json rename to design/charts/1001_albums_you_must_hear_before_you_die.json diff --git a/docs/charts/README.md b/design/charts/README.md similarity index 100% rename from docs/charts/README.md rename to design/charts/README.md diff --git a/docs/charts/chatgpt-prog_rock_metal_top100_2025.json b/design/charts/chatgpt-prog_rock_metal_top100_2025.json similarity index 100% rename from docs/charts/chatgpt-prog_rock_metal_top100_2025.json rename to design/charts/chatgpt-prog_rock_metal_top100_2025.json diff --git a/docs/charts/chatgpt-prog_rock_metal_top100_all_time_chart.json b/design/charts/chatgpt-prog_rock_metal_top100_all_time_chart.json similarity index 100% rename from docs/charts/chatgpt-prog_rock_metal_top100_all_time_chart.json rename to design/charts/chatgpt-prog_rock_metal_top100_all_time_chart.json diff --git a/docs/charts/nmw-the_28_greatest_best_of_albums.json b/design/charts/nmw-the_28_greatest_best_of_albums.json similarity index 100% rename from docs/charts/nmw-the_28_greatest_best_of_albums.json rename to design/charts/nmw-the_28_greatest_best_of_albums.json diff --git a/docs/charts/the_guardian-100_best_albums_ever.json b/design/charts/the_guardian-100_best_albums_ever.json similarity index 100% rename from docs/charts/the_guardian-100_best_albums_ever.json rename to design/charts/the_guardian-100_best_albums_ever.json diff --git a/docs/charts/time-all_time_100_albums_time.json b/design/charts/time-all_time_100_albums_time.json similarity index 100% rename from docs/charts/time-all_time_100_albums_time.json rename to design/charts/time-all_time_100_albums_time.json diff --git a/docs/charts/wikipedia-best_1970s_all_years.json b/design/charts/wikipedia-best_1970s_all_years.json similarity index 100% rename from docs/charts/wikipedia-best_1970s_all_years.json rename to design/charts/wikipedia-best_1970s_all_years.json diff --git a/docs/charts/wikipedia-best_1980s_all_years.json b/design/charts/wikipedia-best_1980s_all_years.json similarity index 100% rename from docs/charts/wikipedia-best_1980s_all_years.json rename to design/charts/wikipedia-best_1980s_all_years.json diff --git a/docs/charts/wikipedia-best_1990s_all_years.json b/design/charts/wikipedia-best_1990s_all_years.json similarity index 100% rename from docs/charts/wikipedia-best_1990s_all_years.json rename to design/charts/wikipedia-best_1990s_all_years.json diff --git a/docs/charts/wikipedia-best_selling_albums_all_tables.json b/design/charts/wikipedia-best_selling_albums_all_tables.json similarity index 100% rename from docs/charts/wikipedia-best_selling_albums_all_tables.json rename to design/charts/wikipedia-best_selling_albums_all_tables.json diff --git a/design/docs/blazor-audio-streaming-authentication.md b/design/docs/blazor-audio-streaming-authentication.md new file mode 100644 index 00000000..cf4d7562 --- /dev/null +++ b/design/docs/blazor-audio-streaming-authentication.md @@ -0,0 +1,267 @@ +# Blazor Audio Streaming Authentication + +## Overview + +This document explains how audio streaming authentication works in Melodee's Blazor Server application, covering both music playback and podcast playback. + +## Authentication Mechanism + +### Cookie-Based Authentication + +Melodee uses a **cookie-based authentication system** for streaming audio from Blazor Server components: + +1. **Cookie Generation** (`MelodeeBlazorCookieMiddleware`) + - Cookie name: `melodee_blazor_token` + - Cookie value: SHA256 hash of `{current_date_yyyyMMdd}{encryption_private_key}` + - Set on every non-API request (Blazor pages) + - Cookie properties: + - `HttpOnly: true` (prevents JavaScript access for security) + - `Secure: true` (requires HTTPS) + - `SameSite: None` (allows cross-site requests for hosted players) + - `Expires: 1 day` + +2. **Cookie Validation** (OpenSubsonic `ControllerBase`) + - All OpenSubsonic endpoints (`/rest/*`) check for the `melodee_blazor_token` cookie + - Cookie is validated by regenerating the hash with current date + encryption key + - If cookie matches, authentication is bypassed (no username/password required) + - Localhost requests and requests from the configured base URL also bypass authentication + +### Why This Works for HTML5 Audio + +When a Blazor component uses JavaScript to load audio via the HTML5 `