From 4b80c7dbb0cff7304548de6c6d92f8d2425e8c8b Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 5 Jul 2026 03:22:24 +0200 Subject: [PATCH] chore: bring flutter client, docs, deploy and tools into monorepo Co-Authored-By: Claude Opus 4.8 --- deploy/seaweedfs/docker-compose.yml | 32 + deploy/seaweedfs/s3.conf | 24 + docs/README.md | 45 + docs/architecture.md | 55 + docs/authentication.md | 40 + docs/development.md | 91 ++ docs/media-processing.md | 46 + docs/projects.md | 59 ++ docs/storage.md | 48 + docs/streaming.md | 55 + .../.agents/skills/accessibility/SKILL.md | 440 ++++++++ .../accessibility/references/A11Y-PATTERNS.md | 233 +++++ .../skills/accessibility/references/WCAG.md | 191 ++++ .../skills/bash-defensive-patterns/SKILL.md | 533 ++++++++++ .../skills/dart-best-practices/SKILL.md | 65 ++ .../skills/flutter-animations/SKILL.md | 116 +++ .../assets/templates/explicit_animation.dart | 345 +++++++ .../assets/templates/hero_transition.dart | 223 ++++ .../assets/templates/implicit_animation.dart | 149 +++ .../assets/templates/staggered_animation.dart | 239 +++++ .../flutter-animations/references/curves.md | 566 ++++++++++ .../flutter-animations/references/explicit.md | 553 ++++++++++ .../flutter-animations/references/hero.md | 724 +++++++++++++ .../flutter-animations/references/implicit.md | 368 +++++++ .../flutter-animations/references/physics.md | 700 +++++++++++++ .../references/staggered.md | 726 +++++++++++++ .../.agents/skills/flutter-expert/SKILL.md | 140 +++ .../flutter-expert/references/bloc-state.md | 259 +++++ .../references/gorouter-navigation.md | 119 +++ .../flutter-expert/references/performance.md | 99 ++ .../references/project-structure.md | 118 +++ .../references/riverpod-state.md | 130 +++ .../references/widget-patterns.md | 123 +++ .../.agents/skills/flutter-testing/SKILL.md | 106 ++ .../references/common-errors.md | 507 +++++++++ .../references/integration-testing.md | 562 ++++++++++ .../flutter-testing/references/mocking.md | 588 +++++++++++ .../references/plugin-testing.md | 493 +++++++++ .../references/unit-testing.md | 433 ++++++++ .../references/widget-testing.md | 670 ++++++++++++ .../scripts/verify-examples.sh | 80 ++ .../skills/frontend-design/LICENSE.txt | 177 ++++ .../.agents/skills/frontend-design/SKILL.md | 42 + musify_flutter/.agents/skills/seo/SKILL.md | 513 +++++++++ musify_flutter/.gitignore | 45 + musify_flutter/.metadata | 45 + musify_flutter/README.md | 3 + musify_flutter/analysis_options.yaml | 9 + musify_flutter/android/.gitignore | 14 + musify_flutter/android/app/build.gradle.kts | 45 + .../android/app/src/main/AndroidManifest.xml | 45 + .../example/musify_flutter/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + musify_flutter/android/build.gradle.kts | 24 + musify_flutter/android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + musify_flutter/android/settings.gradle.kts | 26 + musify_flutter/devtools_options.yaml | 3 + musify_flutter/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 24 + musify_flutter/ios/Flutter/Debug.xcconfig | 1 + musify_flutter/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 644 ++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + musify_flutter/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + musify_flutter/ios/Runner/Info.plist | 70 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + musify_flutter/ios/Runner/SceneDelegate.swift | 6 + .../ios/RunnerTests/RunnerTests.swift | 12 + musify_flutter/lib/main.dart | 8 + musify_flutter/lib/src/app.dart | 20 + .../lib/src/core/auth/auth_providers.dart | 24 + .../lib/src/core/auth/auth_repository.dart | 50 + .../lib/src/core/config/app_config.dart | 23 + .../lib/src/core/network/dio_client.dart | 30 + .../lib/src/core/router/app_router.dart | 65 ++ .../lib/src/core/theme/app_theme.dart | 48 + .../features/home/presentation/home_page.dart | 161 +++ .../library/presentation/library_page.dart | 68 ++ .../player/application/player_controller.dart | 124 +++ .../player/data/stream_repository.dart | 22 + .../features/player/domain/track_stream.dart | 19 + .../player/presentation/mini_player.dart | 124 +++ .../playlists/data/playlist_repository.dart | 87 ++ .../features/playlists/domain/playlist.dart | 34 + .../presentation/playlist_detail_page.dart | 137 +++ .../profile/presentation/profile_page.dart | 158 +++ .../search/presentation/search_page.dart | 45 + .../tracks/data/track_repository.dart | 32 + .../lib/src/features/tracks/domain/track.dart | 22 + .../src/shared/models/paginated_response.dart | 37 + .../src/shared/widgets/async_value_view.dart | 69 ++ .../lib/src/shared/widgets/cover_art.dart | 40 + .../lib/src/shell/scaffold_with_nav.dart | 101 ++ musify_flutter/linux/.gitignore | 1 + musify_flutter/linux/CMakeLists.txt | 128 +++ musify_flutter/linux/flutter/CMakeLists.txt | 88 ++ .../linux/flutter/generated_plugins.cmake | 29 + musify_flutter/linux/runner/CMakeLists.txt | 26 + musify_flutter/linux/runner/main.cc | 6 + musify_flutter/linux/runner/my_application.cc | 148 +++ musify_flutter/linux/runner/my_application.h | 21 + musify_flutter/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 24 + .../macos/Runner.xcodeproj/project.pbxproj | 729 +++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 117 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + musify_flutter/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + musify_flutter/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 12 + musify_flutter/pubspec.lock | 970 ++++++++++++++++++ musify_flutter/pubspec.yaml | 24 + musify_flutter/skills-lock.json | 45 + musify_flutter/windows/.gitignore | 17 + musify_flutter/windows/CMakeLists.txt | 108 ++ musify_flutter/windows/flutter/CMakeLists.txt | 109 ++ .../windows/flutter/generated_plugins.cmake | 29 + musify_flutter/windows/runner/CMakeLists.txt | 40 + musify_flutter/windows/runner/Runner.rc | 121 +++ .../windows/runner/flutter_window.cpp | 71 ++ .../windows/runner/flutter_window.h | 33 + musify_flutter/windows/runner/main.cpp | 43 + musify_flutter/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 14 + musify_flutter/windows/runner/utils.cpp | 69 ++ musify_flutter/windows/runner/utils.h | 19 + .../windows/runner/win32_window.cpp | 288 ++++++ musify_flutter/windows/runner/win32_window.h | 102 ++ pl.json | 1 + player.html | 220 ++++ 189 files changed, 19124 insertions(+) create mode 100644 deploy/seaweedfs/docker-compose.yml create mode 100644 deploy/seaweedfs/s3.conf create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/authentication.md create mode 100644 docs/development.md create mode 100644 docs/media-processing.md create mode 100644 docs/projects.md create mode 100644 docs/storage.md create mode 100644 docs/streaming.md create mode 100644 musify_flutter/.agents/skills/accessibility/SKILL.md create mode 100644 musify_flutter/.agents/skills/accessibility/references/A11Y-PATTERNS.md create mode 100644 musify_flutter/.agents/skills/accessibility/references/WCAG.md create mode 100644 musify_flutter/.agents/skills/bash-defensive-patterns/SKILL.md create mode 100644 musify_flutter/.agents/skills/dart-best-practices/SKILL.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/SKILL.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/assets/templates/explicit_animation.dart create mode 100644 musify_flutter/.agents/skills/flutter-animations/assets/templates/hero_transition.dart create mode 100644 musify_flutter/.agents/skills/flutter-animations/assets/templates/implicit_animation.dart create mode 100644 musify_flutter/.agents/skills/flutter-animations/assets/templates/staggered_animation.dart create mode 100644 musify_flutter/.agents/skills/flutter-animations/references/curves.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/references/explicit.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/references/hero.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/references/implicit.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/references/physics.md create mode 100644 musify_flutter/.agents/skills/flutter-animations/references/staggered.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/SKILL.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/references/bloc-state.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/references/gorouter-navigation.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/references/performance.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/references/project-structure.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/references/riverpod-state.md create mode 100644 musify_flutter/.agents/skills/flutter-expert/references/widget-patterns.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/SKILL.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/references/common-errors.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/references/integration-testing.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/references/mocking.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/references/plugin-testing.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/references/unit-testing.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/references/widget-testing.md create mode 100644 musify_flutter/.agents/skills/flutter-testing/scripts/verify-examples.sh create mode 100644 musify_flutter/.agents/skills/frontend-design/LICENSE.txt create mode 100644 musify_flutter/.agents/skills/frontend-design/SKILL.md create mode 100644 musify_flutter/.agents/skills/seo/SKILL.md create mode 100644 musify_flutter/.gitignore create mode 100644 musify_flutter/.metadata create mode 100644 musify_flutter/README.md create mode 100644 musify_flutter/analysis_options.yaml create mode 100644 musify_flutter/android/.gitignore create mode 100644 musify_flutter/android/app/build.gradle.kts create mode 100644 musify_flutter/android/app/src/main/AndroidManifest.xml create mode 100644 musify_flutter/android/app/src/main/kotlin/com/example/musify_flutter/MainActivity.kt create mode 100644 musify_flutter/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 musify_flutter/android/app/src/main/res/drawable/launch_background.xml create mode 100644 musify_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 musify_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 musify_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 musify_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 musify_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 musify_flutter/android/app/src/main/res/values-night/styles.xml create mode 100644 musify_flutter/android/app/src/main/res/values/styles.xml create mode 100644 musify_flutter/android/app/src/profile/AndroidManifest.xml create mode 100644 musify_flutter/android/build.gradle.kts create mode 100644 musify_flutter/android/gradle.properties create mode 100644 musify_flutter/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 musify_flutter/android/settings.gradle.kts create mode 100644 musify_flutter/devtools_options.yaml create mode 100644 musify_flutter/ios/.gitignore create mode 100644 musify_flutter/ios/Flutter/AppFrameworkInfo.plist create mode 100644 musify_flutter/ios/Flutter/Debug.xcconfig create mode 100644 musify_flutter/ios/Flutter/Release.xcconfig create mode 100644 musify_flutter/ios/Runner.xcodeproj/project.pbxproj create mode 100644 musify_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 musify_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 musify_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 musify_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 musify_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 musify_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 musify_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 musify_flutter/ios/Runner/AppDelegate.swift create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 musify_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 musify_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 musify_flutter/ios/Runner/Base.lproj/Main.storyboard create mode 100644 musify_flutter/ios/Runner/Info.plist create mode 100644 musify_flutter/ios/Runner/Runner-Bridging-Header.h create mode 100644 musify_flutter/ios/Runner/SceneDelegate.swift create mode 100644 musify_flutter/ios/RunnerTests/RunnerTests.swift create mode 100644 musify_flutter/lib/main.dart create mode 100644 musify_flutter/lib/src/app.dart create mode 100644 musify_flutter/lib/src/core/auth/auth_providers.dart create mode 100644 musify_flutter/lib/src/core/auth/auth_repository.dart create mode 100644 musify_flutter/lib/src/core/config/app_config.dart create mode 100644 musify_flutter/lib/src/core/network/dio_client.dart create mode 100644 musify_flutter/lib/src/core/router/app_router.dart create mode 100644 musify_flutter/lib/src/core/theme/app_theme.dart create mode 100644 musify_flutter/lib/src/features/home/presentation/home_page.dart create mode 100644 musify_flutter/lib/src/features/library/presentation/library_page.dart create mode 100644 musify_flutter/lib/src/features/player/application/player_controller.dart create mode 100644 musify_flutter/lib/src/features/player/data/stream_repository.dart create mode 100644 musify_flutter/lib/src/features/player/domain/track_stream.dart create mode 100644 musify_flutter/lib/src/features/player/presentation/mini_player.dart create mode 100644 musify_flutter/lib/src/features/playlists/data/playlist_repository.dart create mode 100644 musify_flutter/lib/src/features/playlists/domain/playlist.dart create mode 100644 musify_flutter/lib/src/features/playlists/presentation/playlist_detail_page.dart create mode 100644 musify_flutter/lib/src/features/profile/presentation/profile_page.dart create mode 100644 musify_flutter/lib/src/features/search/presentation/search_page.dart create mode 100644 musify_flutter/lib/src/features/tracks/data/track_repository.dart create mode 100644 musify_flutter/lib/src/features/tracks/domain/track.dart create mode 100644 musify_flutter/lib/src/shared/models/paginated_response.dart create mode 100644 musify_flutter/lib/src/shared/widgets/async_value_view.dart create mode 100644 musify_flutter/lib/src/shared/widgets/cover_art.dart create mode 100644 musify_flutter/lib/src/shell/scaffold_with_nav.dart create mode 100644 musify_flutter/linux/.gitignore create mode 100644 musify_flutter/linux/CMakeLists.txt create mode 100644 musify_flutter/linux/flutter/CMakeLists.txt create mode 100644 musify_flutter/linux/flutter/generated_plugins.cmake create mode 100644 musify_flutter/linux/runner/CMakeLists.txt create mode 100644 musify_flutter/linux/runner/main.cc create mode 100644 musify_flutter/linux/runner/my_application.cc create mode 100644 musify_flutter/linux/runner/my_application.h create mode 100644 musify_flutter/macos/.gitignore create mode 100644 musify_flutter/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 musify_flutter/macos/Flutter/Flutter-Release.xcconfig create mode 100644 musify_flutter/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 musify_flutter/macos/Runner.xcodeproj/project.pbxproj create mode 100644 musify_flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 musify_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 musify_flutter/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 musify_flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 musify_flutter/macos/Runner/AppDelegate.swift create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 musify_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 musify_flutter/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 musify_flutter/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 musify_flutter/macos/Runner/Configs/Debug.xcconfig create mode 100644 musify_flutter/macos/Runner/Configs/Release.xcconfig create mode 100644 musify_flutter/macos/Runner/Configs/Warnings.xcconfig create mode 100644 musify_flutter/macos/Runner/DebugProfile.entitlements create mode 100644 musify_flutter/macos/Runner/Info.plist create mode 100644 musify_flutter/macos/Runner/MainFlutterWindow.swift create mode 100644 musify_flutter/macos/Runner/Release.entitlements create mode 100644 musify_flutter/macos/RunnerTests/RunnerTests.swift create mode 100644 musify_flutter/pubspec.lock create mode 100644 musify_flutter/pubspec.yaml create mode 100644 musify_flutter/skills-lock.json create mode 100644 musify_flutter/windows/.gitignore create mode 100644 musify_flutter/windows/CMakeLists.txt create mode 100644 musify_flutter/windows/flutter/CMakeLists.txt create mode 100644 musify_flutter/windows/flutter/generated_plugins.cmake create mode 100644 musify_flutter/windows/runner/CMakeLists.txt create mode 100644 musify_flutter/windows/runner/Runner.rc create mode 100644 musify_flutter/windows/runner/flutter_window.cpp create mode 100644 musify_flutter/windows/runner/flutter_window.h create mode 100644 musify_flutter/windows/runner/main.cpp create mode 100644 musify_flutter/windows/runner/resource.h create mode 100644 musify_flutter/windows/runner/resources/app_icon.ico create mode 100644 musify_flutter/windows/runner/runner.exe.manifest create mode 100644 musify_flutter/windows/runner/utils.cpp create mode 100644 musify_flutter/windows/runner/utils.h create mode 100644 musify_flutter/windows/runner/win32_window.cpp create mode 100644 musify_flutter/windows/runner/win32_window.h create mode 100644 pl.json create mode 100644 player.html diff --git a/deploy/seaweedfs/docker-compose.yml b/deploy/seaweedfs/docker-compose.yml new file mode 100644 index 0000000..1d48e3e --- /dev/null +++ b/deploy/seaweedfs/docker-compose.yml @@ -0,0 +1,32 @@ +services: + seaweedfs: + image: chrislusf/seaweedfs:latest + container_name: seaweedfs + # All-in-one dev server: master + volume + filer + S3 gateway. + command: > + server + -ip.bind=0.0.0.0 + -dir=/data + -master.volumeSizeLimitMB=1024 + -volume.max=0 + -filer + -s3 + -s3.config=/etc/seaweedfs/s3.conf + ports: + - "9333:9333" # master (admin/UI) + - "8888:8888" # filer HTTP -> used by the Media Gateway + - "8333:8333" # S3 API -> used by the WebApi (InfrastructureStorage:Address) + # Volume server (8080) is left unpublished on purpose: it only needs to be + # reachable internally, and 8080 clashes with Zitadel on the host. + volumes: + - seaweedfs_data:/data + - ./s3.conf:/etc/seaweedfs/s3.conf:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + seaweedfs_data: diff --git a/deploy/seaweedfs/s3.conf b/deploy/seaweedfs/s3.conf new file mode 100644 index 0000000..dc0ed2a --- /dev/null +++ b/deploy/seaweedfs/s3.conf @@ -0,0 +1,24 @@ +{ + "identities": [ + { + "name": "admin_user", + "credentials": [ + { + "accessKey": "admin_access_key", + "secretKey": "admin_secret_key" + } + ], + "actions": ["Admin", "Read", "Write"] + }, + { + "name": "read_only_user", + "credentials": [ + { + "accessKey": "readonly_access_key", + "secretKey": "readonly_secret_key" + } + ], + "actions": ["Read"] + } + ] +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..963c0d0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,45 @@ +# Musify — Documentación + +Musify es un backend de streaming de música: subir pistas, procesarlas (transcodificar el audio a `.m4a` y generar miniaturas), organizarlas en playlists y reproducirlas en streaming sin que los bytes pasen por la API. + +Esta carpeta explica **qué hace cada pieza, su propósito y por qué está diseñada así**. + +## Índice + +- [architecture.md](architecture.md) — Visión global: componentes, cómo encajan y los flujos principales. +- [projects.md](projects.md) — Qué es y para qué sirve cada proyecto/solución. +- [authentication.md](authentication.md) — Zitadel (OIDC), validación de JWT, provisioning/sync de usuarios. +- [storage.md](storage.md) — SeaweedFS (S3), estructura de claves, URLs prefirmadas, qué es público y qué privado. +- [streaming.md](streaming.md) — Streaming del audio (`.m4a` por HTTP range) con stream-ticket (RS256) y el reverse proxy (StreamingGateway). +- [media-processing.md](media-processing.md) — El worker y los workflows de MassTransit (transcode de audio, miniaturas). +- [development.md](development.md) — Cómo levantar todo en local: contenedores, puertos y comandos. + +## Mapa de componentes (alto nivel) + +``` + ┌─────────────────────────────────────────────┐ + Cliente ───► │ WebApi (:5111) API REST + emite tickets │ + └─────────────────────────────────────────────┘ + │ EF Core │ presigned (subidas) │ MassTransit (eventos) + ▼ ▼ ▼ + PostgreSQL SeaweedFS S3 (:8333) RabbitMQ ──► Worker (procesa) + (metadatos) (objetos) audio→.m4a, imágenes + ▲ + Cliente ───► StreamingGateway (:8081) ┘ sirve audio .m4a (valida ticket, proxy al filer) + + Login: Cliente ◄──► Zitadel (:8080, OIDC) → WebApi valida el JWT +``` + +## Stack + +- **.NET 10**, C#. Minimal APIs (WebApi), Worker Service (Worker), ASP.NET + YARP (StreamingGateway). +- **CQRS** con [Mediator] (source-generator) y resultados con **Ardalis.Result**. +- **EF Core 10** + **PostgreSQL** (metadatos). +- **MassTransit 8** + **RabbitMQ** (procesado asíncrono con routing slips/sagas). +- **SeaweedFS** (almacenamiento de objetos compatible con S3, vía AWSSDK.S3). +- **ffmpeg** (transcode de audio a AAC/`.m4a`), **SixLabors.ImageSharp** (miniaturas). +- **Zitadel** (OIDC/OAuth2) para identidad. + +## Repositorios git + +Cada proyecto de nivel superior es su propio repo git: `MusifyBackend`, `Musify.WebApi`, `Musify.StreamingGateway`, `Musify.Worker`. La raíz del workspace (`docs/`, `deploy/`, `player.html`) no está versionada. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9226e1c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,55 @@ +# Arquitectura + +## Principios + +- **Clean architecture / capas** en `MusifyBackend`: el dominio y la aplicación no dependen de infraestructura; la infraestructura implementa las interfaces (`Application/Contracts`). +- **CQRS**: cada operación es un `Command` o `Query` con su handler (Mediator). Los handlers devuelven `Ardalis.Result` (Ok/NotFound/Unauthorized/Conflict/Invalid…), que la WebApi traduce a códigos HTTP. +- **Los bytes pesados no pasan por la API**: subidas con URLs prefirmadas (cliente → S3) y reproducción vía un reverse proxy dedicado (gateway → S3). La WebApi solo mueve metadatos y emite permisos. +- **Procesado asíncrono**: crear una pista no transcodifica en caliente; publica un evento y un worker hace el trabajo pesado con compensación (sagas/routing slips de MassTransit). + +## Componentes + +| Componente | Responsabilidad | Por qué separado | +|---|---|---| +| **WebApi** | API REST (usuarios, pistas, playlists), autoriza, emite presigned URLs y stream-tickets | Punto de entrada fino; no procesa medios ni sirve bytes | +| **MusifyBackend** | Dominio + casos de uso + infraestructura compartida (EF, S3, MassTransit, ffmpeg) | Lógica reutilizada por WebApi y Worker | +| **StreamingGateway** | Reverse proxy (YARP) que valida el ticket y sirve el audio (`.m4a`) desde SeaweedFS | Escala con el tráfico de bytes, independiente de la API; SeaweedFS queda privado detrás | +| **Worker** | Consume eventos de MassTransit: transcodifica audio a `.m4a` y genera miniaturas | Trabajo CPU/IO intensivo fuera del request HTTP | +| **PostgreSQL** | Metadatos (usuarios, pistas, playlists, upload intents) | — | +| **RabbitMQ** | Cola de eventos + outbox transaccional | Desacopla creación de procesado | +| **SeaweedFS** | Almacenamiento de objetos (S3 + filer) | Guarda originales y derivados (audio `.m4a`, miniaturas) | +| **Zitadel** | Identidad (OIDC/OAuth2) | Login y emisión de JWT | + +## Flujos principales + +### 1. Subida de una pista +1. Cliente pide `POST /tracks/upload-urls` → la WebApi crea **UploadIntents** (reservas) y devuelve **URLs prefirmadas PUT** (imagen + audio). +2. Cliente sube los ficheros **directo a SeaweedFS** con esas URLs (no pasan por la API). +3. Cliente llama `POST /tracks` con los `intentId`s → la WebApi valida los intents, crea el `Track` (+ `UserHasTrack`) y publica `CreateTrackResourcesEvent`. +4. El **Worker** consume el evento: mueve los originales a su sitio, genera miniaturas y transcodifica el audio a `.m4a`, y actualiza el estado del track. + +Ver detalle en [media-processing.md](media-processing.md) y [storage.md](storage.md). + +### 2. Reproducción (streaming) +1. Cliente pide `GET /tracks/{id}/stream` (autenticado) → la WebApi autoriza y devuelve `{ manifestUrl, ticket }` (URL al `.m4a` + ticket RS256 acotado a la carpeta de esa pista). +2. El reproductor abre la URL del audio añadiendo `?t=` (o cabecera `X-Stream-Ticket`) y descarga por HTTP range. +3. El **StreamingGateway** valida el ticket y reenvía al filer de SeaweedFS; los bytes van storage → cliente. + +Ver [streaming.md](streaming.md). + +### 3. Autenticación +1. Cliente hace login OIDC contra **Zitadel** y obtiene un access token (JWT). +2. La WebApi valida la firma/iss/aud del JWT. +3. En cada token validado, se **sincroniza** el usuario local (crea/actualiza nombre y foto) — provisioning just-in-time. + +Ver [authentication.md](authentication.md). + +## Modelo de datos (entidades) + +- **User** — `Id` es el `sub` numérico de Zitadel (`long`), más nombre y `ProfilePictureUrl`. +- **Track** — pista: nombres de originales/derivados, `AudioFolderName` (carpeta del `.m4a` transcodificado), estados de procesado (`ProcessingStatus`), `LifeCycleStatus`. +- **PlayList** — lista del usuario (`UserId`). +- **PlayListHasTrack** — pistas dentro de una playlist (con `Position`). +- **UserHasTrack** — propiedad: qué usuario posee qué pista. +- **UploadIntent** — reserva de subida (clave, bucket, tamaño esperado, estado, expiración) para validar y aplicar cuota. +- **Upload** — registro de objetos subidos/transferidos al storage. diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..0529118 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,40 @@ +# Autenticación e identidad + +## Proveedor: Zitadel (OIDC/OAuth2) + +El login y la emisión de tokens los hace **Zitadel** (no la WebApi). La WebApi es un **resource server**: solo **valida** el access token (JWT) que le llega en `Authorization: Bearer`. + +- Config en la sección `Authentication` (`AuthenticationConfiguration`): metadata address, issuer, audience, client id, endpoints de authorize/token, scopes. +- `JwtBearerOptionsSetup` configura la validación (issuer, audience, claves vía metadata OIDC). +- En dev, `RequireHttpsMetadata = false`. + +### Requisito clave en la app de Zitadel +- **Auth Token Type = JWT** (si no, Zitadel emite un token **opaco** sin puntos y `JwtBearer` no lo puede validar → 401 en todo lo protegido). +- **User Info inside Token** activado si quieres que el access token traiga claims de perfil (`name`, `email`, `picture`). Por defecto el access token es mínimo (`sub`, `aud`, `iss`, `exp`…), sin perfil. + +## El `sub` es numérico → `User.Id` es `long` + +El `sub` de Zitadel es un entero de 64 bits en string (p. ej. `371953080444977155`), **no un GUID**. Por eso: +- `User.Id` y todas las FKs de usuario (`PlayList.UserId`, `UserHasTrack.UserId`, `UploadIntent.UserId`) son **`long`**. +- `User.Id` lleva `[DatabaseGenerated(None)]` para que EF **no** lo autogenere: se asigna el `sub`. +- `CurrentUser.Id` es `long?` (parsea con `long.TryParse`); `RequiredId` lanza si falta. + +**Por qué**: el código antiguo hacía `Guid.Parse(sub)` y reventaba (FormatException) → 401 en cada endpoint protegido. Se decidió guardar el `sub` tal cual como número. + +## Provisioning / sincronización de usuarios + +En cada token validado, `JwtBearerEventsHandler.TokenValidated`: +1. Lee `sub` (id), `name`/`preferred_username`/`email` (con fallback) y el claim OIDC `picture`. +2. Envía un `SyncUserCommand`. + +`SyncUserCommandHandler` hace un **upsert idempotente**: +- Si el usuario no existe → lo crea. +- Si existe → actualiza nombre/apellidos/`ProfilePictureUrl` **solo si cambiaron** (escribe en DB únicamente cuando hay cambios). + +**Por qué upsert con "escribe solo si cambió"**: `TokenValidated` se ejecuta en **cada request**; sin ese guardado condicional estaríamos escribiendo en cada petición. Y debe ser resiliente: si el provisioning falla, se loguea pero **no** tumba la autenticación. + +> Nota: `CreateUserCommand` (endpoint manual `POST /users`) sigue siendo create-only; el sync del login usa `SyncUserCommand`. + +## Foto de perfil + +`User.ProfilePictureUrl` se rellena desde el claim `picture` del token. Si Zitadel no mete la info de perfil en el access token (ver arriba), llega `null` — el código es correcto, pero no hay nada que sincronizar hasta activar esa opción. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..401d83a --- /dev/null +++ b/docs/development.md @@ -0,0 +1,91 @@ +# Entorno de desarrollo + +Cómo levantar todo en local y probarlo. + +## Dependencias (Docker) + +```powershell +# SeaweedFS (almacenamiento) +docker compose -f deploy\seaweedfs\docker-compose.yml up -d + +# PostgreSQL + pgAdmin +docker compose -f "C:\Users\XShin\OneDrive\Desktop\postgresql\docker-compose.yml" up -d + +# Zitadel (auth) +docker compose -f "C:\Users\XShin\OneDrive\Pictures\zitadel-compose\docker-compose.yml" up -d + +# RabbitMQ (contenedor suelto) +docker start rabbitmq +``` + +> Nota: usa `docker compose up` (no `docker start`) para Postgres/Zitadel: si sus redes se borraron, compose las recrea. + +## Aplicaciones (.NET) + +```powershell +dotnet run --project Musify.WebApi\WebApi # :5111 +dotnet run --project Musify.StreamingGateway\StreamingGateway # :8081 +dotnet run --project Musify.Worker\Worker # background +``` + +## Mapa de puertos + +| Servicio | Puerto | +|---|---| +| WebApi | 5111 (`/scalar/v1`, `/openapi/v1.json`) | +| StreamingGateway | 8081 (`/health`, `/media/...`) | +| SeaweedFS S3 / filer / master | 8333 / 8888 / 9333 | +| Zitadel | 8080 | +| PostgreSQL | 59000 (→ 5432 en el contenedor) | +| RabbitMQ AMQP / management | 5672 / 15672 | +| pgAdmin | 5050 | + +DB: `musify_db`, usuario `postgres`/`postgres`. Bucket S3: `webapi-storage` (credenciales `admin_access_key`/`admin_secret_key`). + +## Migraciones + +No hay migración automática al arrancar. Para aplicar cambios de esquema: + +```powershell +cd MusifyBackend\Infrastructure +dotnet ef database update --context Database +``` + +La cadena de conexión para `dotnet ef` se lee de `DesignSettings.json` / user-secrets del proyecto Infrastructure. + +## Stream-tickets (claves RS256) + +La WebApi firma los stream-tickets con una clave privada y el gateway valida con la pública: + +```powershell +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out stream_private.pem +openssl rsa -in stream_private.pem -pubout -out stream_public.pem +``` + +- WebApi: `StreamTicket:PrivateKeyPath` → `stream_private.pem`. +- Gateway: `StreamTicket:PublicKeyPath` → `stream_public.pem`. + +## Consola de pruebas: `player.html` + +Un único archivo HTML para probar todo sin front: + +1. Sírvelo o ábrelo directo (el CORS está abierto en Development). +2. Pega tu **access token** de Zitadel y la API base (`http://localhost:5111`). +3. Permite: crear/actualizar/eliminar playlists, listar canciones, añadir/quitar canciones de una playlist, ver las canciones de una playlist y **reproducir** (pide el ticket y abre el `.m4a` con `?t=`). + +> Nota: `player.html` todavía usa dash.js; tras el cambio a `.m4a` debe actualizarse para reproducir el fichero directo (elemento `