From 031cab75e0051486e19890cf7a0d363d9a7b2743 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 27 Aug 2024 01:49:05 +0800 Subject: [PATCH] :sparkles: Playback server --- ios/Podfile.lock | 43 + lib/collections/assets.gen.dart | 192 +++++ lib/collections/formatters.dart | 8 + lib/collections/gradients.dart | 232 ++++++ lib/collections/initializers.dart | 26 + lib/collections/intents.dart | 88 ++ lib/collections/language_codes.dart | 757 ++++++++++++++++++ lib/collections/spotify_markets.dart | 189 +++++ lib/main.dart | 13 + lib/providers/audio_player.dart | 122 +++ lib/providers/piped.dart | 3 - lib/router.dart | 14 +- lib/services/artist.dart | 7 + lib/services/audio_player/state.dart | 108 +++ .../audio_services/audio_services.dart | 84 ++ lib/services/audio_services/image.dart | 34 + .../audio_services/mobile_audio_service.dart | 153 ++++ .../audio_services/windows_audio_service.dart | 101 +++ lib/services/primitive.dart | 53 ++ lib/services/server/active_sourced_track.dart | 39 + lib/services/server/routes/playback.dart | 66 ++ lib/services/server/server.dart | 48 ++ lib/services/server/sourced_track.dart | 17 + lib/widgets/tracks/playlist_track_list.dart | 5 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 + pubspec.lock | 228 +++++- pubspec.yaml | 9 + windows/flutter/generated_plugins.cmake | 1 + 28 files changed, 2634 insertions(+), 12 deletions(-) create mode 100755 lib/collections/assets.gen.dart create mode 100755 lib/collections/formatters.dart create mode 100755 lib/collections/gradients.dart create mode 100755 lib/collections/initializers.dart create mode 100755 lib/collections/intents.dart create mode 100755 lib/collections/language_codes.dart create mode 100755 lib/collections/spotify_markets.dart create mode 100644 lib/providers/audio_player.dart delete mode 100644 lib/providers/piped.dart create mode 100644 lib/services/artist.dart create mode 100644 lib/services/audio_player/state.dart create mode 100755 lib/services/audio_services/audio_services.dart create mode 100644 lib/services/audio_services/image.dart create mode 100755 lib/services/audio_services/mobile_audio_service.dart create mode 100755 lib/services/audio_services/windows_audio_service.dart create mode 100755 lib/services/primitive.dart create mode 100755 lib/services/server/active_sourced_track.dart create mode 100755 lib/services/server/routes/playback.dart create mode 100755 lib/services/server/server.dart create mode 100755 lib/services/server/sourced_track.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 488d1f2..644e06b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,34 +1,77 @@ PODS: + - audio_service (0.0.1): + - Flutter + - audio_session (0.0.1): + - Flutter + - device_info_plus (0.0.1): + - Flutter - Flutter (1.0.0) + - flutter_broadcasts (0.0.1): + - Flutter + - media_kit_libs_ios_audio (1.0.4): + - Flutter + - media_kit_native_event_loop (1.0.0): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - sqflite (0.0.3): - Flutter - FlutterMacOS DEPENDENCIES: + - audio_service (from `.symlinks/plugins/audio_service/ios`) + - audio_session (from `.symlinks/plugins/audio_session/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) + - flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`) + - media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`) + - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) EXTERNAL SOURCES: + audio_service: + :path: ".symlinks/plugins/audio_service/ios" + audio_session: + :path: ".symlinks/plugins/audio_session/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter + flutter_broadcasts: + :path: ".symlinks/plugins/flutter_broadcasts/ios" + media_kit_libs_ios_audio: + :path: ".symlinks/plugins/media_kit_libs_ios_audio/ios" + media_kit_native_event_loop: + :path: ".symlinks/plugins/media_kit_native_event_loop/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/darwin" SPEC CHECKSUMS: + audio_service: f509d65da41b9521a61f1c404dd58651f265a567 + audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882 + media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3 + media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart new file mode 100755 index 0000000..1f36103 --- /dev/null +++ b/lib/collections/assets.gen.dart @@ -0,0 +1,192 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +import 'package:flutter/widgets.dart'; + +class $AssetsLogosGen { + const $AssetsLogosGen(); + + /// File path: assets/logos/songlink-transparent.png + AssetGenImage get songlinkTransparent => + const AssetGenImage('assets/logos/songlink-transparent.png'); + + /// File path: assets/logos/songlink.png + AssetGenImage get songlink => + const AssetGenImage('assets/logos/songlink.png'); + + /// List of all assets + List get values => [songlinkTransparent, songlink]; +} + +class $AssetsTutorialGen { + const $AssetsTutorialGen(); + + /// File path: assets/tutorial/step-1.png + AssetGenImage get step1 => const AssetGenImage('assets/tutorial/step-1.png'); + + /// File path: assets/tutorial/step-2.png + AssetGenImage get step2 => const AssetGenImage('assets/tutorial/step-2.png'); + + /// File path: assets/tutorial/step-3.png + AssetGenImage get step3 => const AssetGenImage('assets/tutorial/step-3.png'); + + /// List of all assets + List get values => [step1, step2, step3]; +} + +class Assets { + Assets._(); + + static const AssetGenImage albumPlaceholder = + AssetGenImage('assets/album-placeholder.png'); + static const AssetGenImage bengaliPatternsBg = + AssetGenImage('assets/bengali-patterns-bg.jpg'); + static const AssetGenImage branding = AssetGenImage('assets/branding.png'); + static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); + static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png'); + static const AssetGenImage likedTracks = + AssetGenImage('assets/liked-tracks.jpg'); + static const $AssetsLogosGen logos = $AssetsLogosGen(); + static const AssetGenImage placeholder = + AssetGenImage('assets/placeholder.png'); + static const AssetGenImage rhythm_boxHeroBanner = + AssetGenImage('assets/rhythm_box-hero-banner.png'); + static const AssetGenImage rhythm_boxLogoForeground = + AssetGenImage('assets/rhythm_box-logo-foreground.jpg'); + static const String rhythm_boxLogoIco = 'assets/rhythm_box-logo.ico'; + static const AssetGenImage rhythm_boxLogoPng = + AssetGenImage('assets/rhythm_box-logo.png'); + static const String rhythm_boxLogoSvg = 'assets/rhythm_box-logo.svg'; + static const AssetGenImage rhythm_boxLogoAndroid12 = + AssetGenImage('assets/rhythm_box-logo_android12.png'); + static const AssetGenImage rhythm_boxNightlyLogoForeground = + AssetGenImage('assets/rhythm_box-nightly-logo-foreground.jpg'); + static const AssetGenImage rhythm_boxNightlyLogoPng = + AssetGenImage('assets/rhythm_box-nightly-logo.png'); + static const String rhythm_boxNightlyLogoSvg = + 'assets/rhythm_box-nightly-logo.svg'; + static const AssetGenImage rhythm_boxNightlyLogoAndroid12 = + AssetGenImage('assets/rhythm_box-nightly-logo_android12.png'); + static const AssetGenImage rhythm_boxScreenshot = + AssetGenImage('assets/rhythm_box-screenshot.png'); + static const AssetGenImage rhythm_boxTallCapsule = + AssetGenImage('assets/rhythm_box-tall-capsule.png'); + static const AssetGenImage rhythm_boxWideCapsuleLarge = + AssetGenImage('assets/rhythm_box-wide-capsule-large.png'); + static const AssetGenImage rhythm_boxWideCapsuleSmall = + AssetGenImage('assets/rhythm_box-wide-capsule-small.png'); + static const AssetGenImage rhythm_boxBanner = + AssetGenImage('assets/rhythm_box_banner.png'); + static const AssetGenImage success = AssetGenImage('assets/success.png'); + static const $AssetsTutorialGen tutorial = $AssetsTutorialGen(); + static const AssetGenImage userPlaceholder = + AssetGenImage('assets/user-placeholder.png'); + + /// List of all assets + static List get values => [ + albumPlaceholder, + bengaliPatternsBg, + branding, + emptyBox, + jiosaavn, + likedTracks, + placeholder, + rhythm_boxHeroBanner, + rhythm_boxLogoForeground, + rhythm_boxLogoIco, + rhythm_boxLogoPng, + rhythm_boxLogoSvg, + rhythm_boxLogoAndroid12, + rhythm_boxNightlyLogoForeground, + rhythm_boxNightlyLogoPng, + rhythm_boxNightlyLogoSvg, + rhythm_boxNightlyLogoAndroid12, + rhythm_boxScreenshot, + rhythm_boxTallCapsule, + rhythm_boxWideCapsuleLarge, + rhythm_boxWideCapsuleSmall, + rhythm_boxBanner, + success, + userPlaceholder + ]; +} + +class AssetGenImage { + const AssetGenImage(this._assetName); + + final String _assetName; + + Image image({ + Key? key, + AssetBundle? bundle, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? scale, + double? width, + double? height, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + String? package, + FilterQuality filterQuality = FilterQuality.low, + int? cacheWidth, + int? cacheHeight, + }) { + return Image.asset( + _assetName, + key: key, + bundle: bundle, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + scale: scale, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + package: package, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } + + ImageProvider provider({ + AssetBundle? bundle, + String? package, + }) { + return AssetImage( + _assetName, + bundle: bundle, + package: package, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/lib/collections/formatters.dart b/lib/collections/formatters.dart new file mode 100755 index 0000000..0aed9e9 --- /dev/null +++ b/lib/collections/formatters.dart @@ -0,0 +1,8 @@ +import 'package:intl/intl.dart'; + +final compactNumberFormatter = NumberFormat.compact(); +final usdFormatter = NumberFormat.compactCurrency( + locale: 'en-US', + symbol: r"$", + decimalDigits: 2, +); diff --git a/lib/collections/gradients.dart b/lib/collections/gradients.dart new file mode 100755 index 0000000..e861dde --- /dev/null +++ b/lib/collections/gradients.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; + +const gradients = [ + LinearGradient(colors: [ + Color.fromRGBO(123, 102, 255, 1), + Color.fromRGBO(95, 189, 255, 1), + Color.fromRGBO(150, 239, 255, 1), + Color.fromRGBO(197, 255, 248, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(245, 204, 160, 1), + Color.fromRGBO(228, 143, 69, 1), + Color.fromRGBO(153, 77, 28, 1), + Color.fromRGBO(107, 36, 12, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(243, 243, 243, 1), + Color.fromRGBO(197, 232, 152, 1), + Color.fromRGBO(41, 173, 178, 1), + Color.fromRGBO(7, 102, 173, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(240, 89, 65, 1), + Color.fromRGBO(190, 49, 68, 1), + Color.fromRGBO(135, 35, 65, 1), + Color.fromRGBO(34, 9, 44, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(119, 107, 93, 1), + Color.fromRGBO(176, 166, 149, 1), + Color.fromRGBO(235, 227, 213, 1), + Color.fromRGBO(243, 238, 234, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(208, 162, 247, 1), + Color.fromRGBO(220, 191, 255, 1), + Color.fromRGBO(229, 212, 255, 1), + Color.fromRGBO(241, 234, 255, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(221, 242, 253, 1), + Color.fromRGBO(155, 190, 200, 1), + Color.fromRGBO(66, 125, 157, 1), + Color.fromRGBO(22, 72, 99, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(119, 67, 219, 1), + Color.fromRGBO(195, 172, 208, 1), + Color.fromRGBO(247, 239, 229, 1), + Color.fromRGBO(255, 251, 245, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(194, 217, 255, 1), + Color.fromRGBO(142, 143, 250, 1), + Color.fromRGBO(119, 82, 254, 1), + Color.fromRGBO(25, 4, 130, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(104, 126, 255, 1), + Color.fromRGBO(128, 179, 255, 1), + Color.fromRGBO(152, 228, 255, 1), + Color.fromRGBO(182, 255, 250, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(176, 87, 141, 1), + Color.fromRGBO(217, 136, 185, 1), + Color.fromRGBO(250, 203, 234, 1), + Color.fromRGBO(255, 228, 214, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(190, 255, 247, 1), + Color.fromRGBO(166, 246, 255, 1), + Color.fromRGBO(158, 221, 255, 1), + Color.fromRGBO(100, 153, 233, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(245, 252, 205, 1), + Color.fromRGBO(120, 214, 198, 1), + Color.fromRGBO(65, 145, 151, 1), + Color.fromRGBO(18, 72, 107, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(229, 207, 247, 1), + Color.fromRGBO(157, 118, 193, 1), + Color.fromRGBO(113, 58, 190, 1), + Color.fromRGBO(91, 8, 136, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(249, 222, 201, 1), + Color.fromRGBO(247, 140, 162, 1), + Color.fromRGBO(216, 0, 50, 1), + Color.fromRGBO(61, 12, 17, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(242, 247, 161, 1), + Color.fromRGBO(53, 162, 159, 1), + Color.fromRGBO(8, 131, 149, 1), + Color.fromRGBO(7, 25, 82, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(243, 159, 90, 1), + Color.fromRGBO(174, 68, 90, 1), + Color.fromRGBO(102, 37, 73, 1), + Color.fromRGBO(69, 25, 82, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 200, 200, 1), + Color.fromRGBO(255, 155, 130, 1), + Color.fromRGBO(255, 63, 164, 1), + Color.fromRGBO(87, 55, 93, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(238, 238, 238, 1), + Color.fromRGBO(100, 204, 197, 1), + Color.fromRGBO(23, 107, 135, 1), + Color.fromRGBO(5, 59, 80, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(198, 61, 47, 1), + Color.fromRGBO(226, 94, 62, 1), + Color.fromRGBO(255, 155, 80, 1), + Color.fromRGBO(255, 187, 92, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(236, 83, 176, 1), + Color.fromRGBO(157, 68, 192, 1), + Color.fromRGBO(77, 45, 183, 1), + Color.fromRGBO(14, 33, 160, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(242, 236, 190, 1), + Color.fromRGBO(226, 199, 153, 1), + Color.fromRGBO(192, 130, 97, 1), + Color.fromRGBO(154, 59, 59, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 253, 140, 1), + Color.fromRGBO(151, 255, 244, 1), + Color.fromRGBO(112, 145, 245, 1), + Color.fromRGBO(121, 63, 223, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(67, 83, 52, 1), + Color.fromRGBO(158, 179, 132, 1), + Color.fromRGBO(206, 222, 189, 1), + Color.fromRGBO(250, 241, 228, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(250, 240, 230, 1), + Color.fromRGBO(185, 180, 199, 1), + Color.fromRGBO(92, 84, 112, 1), + Color.fromRGBO(53, 47, 68, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 186, 134, 1), + Color.fromRGBO(246, 99, 92, 1), + Color.fromRGBO(194, 51, 115, 1), + Color.fromRGBO(121, 21, 91, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(213, 255, 208, 1), + Color.fromRGBO(64, 248, 255, 1), + Color.fromRGBO(39, 158, 255, 1), + Color.fromRGBO(12, 53, 106, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(131, 96, 150, 1), + Color.fromRGBO(237, 123, 123, 1), + Color.fromRGBO(240, 184, 110, 1), + Color.fromRGBO(235, 231, 108, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(63, 29, 56, 1), + Color.fromRGBO(77, 60, 119, 1), + Color.fromRGBO(162, 103, 138, 1), + Color.fromRGBO(225, 152, 152, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(254, 123, 229, 1), + Color.fromRGBO(151, 78, 195, 1), + Color.fromRGBO(80, 64, 153, 1), + Color.fromRGBO(49, 56, 102, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(248, 222, 34, 1), + Color.fromRGBO(249, 76, 16, 1), + Color.fromRGBO(199, 0, 57, 1), + Color.fromRGBO(144, 12, 63, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(101, 69, 31, 1), + Color.fromRGBO(118, 88, 39, 1), + Color.fromRGBO(200, 174, 125, 1), + Color.fromRGBO(234, 198, 150, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 246, 224, 1), + Color.fromRGBO(216, 217, 218, 1), + Color.fromRGBO(97, 103, 122, 1), + Color.fromRGBO(39, 40, 41, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(145, 109, 179, 1), + Color.fromRGBO(228, 133, 134, 1), + Color.fromRGBO(252, 186, 173, 1), + Color.fromRGBO(253, 229, 236, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(124, 115, 192, 1), + Color.fromRGBO(148, 173, 215, 1), + Color.fromRGBO(172, 250, 223, 1), + Color.fromRGBO(232, 255, 206, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(174, 216, 204, 1), + Color.fromRGBO(205, 102, 136, 1), + Color.fromRGBO(122, 49, 111, 1), + Color.fromRGBO(70, 25, 89, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(237, 228, 255, 1), + Color.fromRGBO(215, 187, 245, 1), + Color.fromRGBO(160, 118, 249, 1), + Color.fromRGBO(101, 40, 247, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 236, 175, 1), + Color.fromRGBO(255, 176, 127, 1), + Color.fromRGBO(255, 82, 162, 1), + Color.fromRGBO(243, 21, 89, 1) + ]), +]; diff --git a/lib/collections/initializers.dart b/lib/collections/initializers.dart new file mode 100755 index 0000000..5c1c4ab --- /dev/null +++ b/lib/collections/initializers.dart @@ -0,0 +1,26 @@ +import 'dart:io'; + +import 'package:rhythm_box/platform.dart'; +import 'package:win32_registry/win32_registry.dart'; + +Future registerWindowsScheme(String scheme) async { + if (!PlatformInfo.isWindows) return; + String appPath = Platform.resolvedExecutable; + + String protocolRegKey = 'Software\\Classes\\$scheme'; + RegistryValue protocolRegValue = const RegistryValue( + 'URL Protocol', + RegistryValueType.string, + '', + ); + String protocolCmdRegKey = 'shell\\open\\command'; + RegistryValue protocolCmdRegValue = RegistryValue( + '', + RegistryValueType.string, + '"$appPath" "%1"', + ); + + final regKey = Registry.currentUser.createKey(protocolRegKey); + regKey.createValue(protocolRegValue); + regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); +} diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart new file mode 100755 index 0000000..aec6fb4 --- /dev/null +++ b/lib/collections/intents.dart @@ -0,0 +1,88 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:rhythm_box/platform.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; + +class PlayPauseIntent extends Intent { + const PlayPauseIntent(); +} + +class PlayPauseAction extends Action { + @override + invoke(intent) async { + if (!audioPlayer.isPlaying) { + await audioPlayer.resume(); + } else { + await audioPlayer.pause(); + } + return null; + } +} + +class NavigationIntent extends Intent { + final GoRouter router; + final String path; + const NavigationIntent(this.router, this.path); +} + +class NavigationAction extends Action { + @override + invoke(intent) { + intent.router.go(intent.path); + return null; + } +} + +enum HomeTabs { + browse, + search, + library, + lyrics, +} + +class HomeTabIntent extends Intent { + final HomeTabs tab; + const HomeTabIntent({required this.tab}); +} + +class HomeTabAction extends Action { + @override + invoke(intent) { + return null; + } +} + +class SeekIntent extends Intent { + final bool forward; + const SeekIntent(this.forward); +} + +class SeekAction extends Action { + @override + invoke(intent) async { + final position = audioPlayer.position.inSeconds; + await audioPlayer.seek( + Duration( + seconds: intent.forward ? position + 5 : position - 5, + ), + ); + return null; + } +} + +class CloseAppIntent extends Intent {} + +class CloseAppAction extends Action { + @override + invoke(intent) { + if (PlatformInfo.isDesktop) { + exit(0); + } else { + SystemNavigator.pop(); + } + return null; + } +} diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart new file mode 100755 index 0000000..44da6ee --- /dev/null +++ b/lib/collections/language_codes.dart @@ -0,0 +1,757 @@ +class ISOLanguageName { + final String name; + final String nativeName; + + const ISOLanguageName({ + required this.name, + required this.nativeName, + }); + + @override + String toString() { + return "$name ($nativeName)"; + } +} + +// Uncomment the languages as we add support for them +// Currently supported: bn,en,fr,hi,zh +abstract class LanguageLocals { + static final Map isoLangs = { + // "ab": const ISOLanguageName( + // name: "Abkhaz", + // nativeName: "аҧсуа", + // ), + // "aa": const ISOLanguageName( + // name: "Afar", + // nativeName: "Afaraf", + // ), + // "af": const ISOLanguageName( + // name: "Afrikaans", + // nativeName: "Afrikaans", + // ), + // "ak": const ISOLanguageName( + // name: "Akan", + // nativeName: "Akan", + // ), + // "sq": const ISOLanguageName( + // name: "Albanian", + // nativeName: "Shqip", + // ), + // "am": const ISOLanguageName( + // name: "Amharic", + // nativeName: "አማርኛ", + // ), + "ar": const ISOLanguageName( + name: "Arabic", + nativeName: "العربية", + ), + // "an": const ISOLanguageName( + // name: "Aragonese", + // nativeName: "Aragonés", + // ), + // "hy": const ISOLanguageName( + // name: "Armenian", + // nativeName: "Հայերեն", + // ), + // "as": const ISOLanguageName( + // name: "Assamese", + // nativeName: "অসমীয়া", + // ), + // "av": const ISOLanguageName( + // name: "Avaric", + // nativeName: "авар мацӀ, магӀарул мацӀ", + // ), + // "ae": const ISOLanguageName( + // name: "Avestan", + // nativeName: "avesta", + // ), + // "ay": const ISOLanguageName( + // name: "Aymara", + // nativeName: "aymar aru", + // ), + // "az": const ISOLanguageName( + // name: "Azerbaijani", + // nativeName: "azərbaycan dili", + // ), + // "bm": const ISOLanguageName( + // name: "Bambara", + // nativeName: "bamanankan", + // ), + // "ba": const ISOLanguageName( + // name: "Bashkir", + // nativeName: "башҡорт теле", + // ), + "eu": const ISOLanguageName( + name: "Basque", + nativeName: "Euskara", + ), + // "be": const ISOLanguageName( + // name: "Belarusian", + // nativeName: "Беларуская", + // ), + "bn": const ISOLanguageName( + name: "Bengali", + nativeName: "বাংলা", + ), + // "bh": const ISOLanguageName( + // name: "Bihari", + // nativeName: "भोजपुरी", + // ), + // "bi": const ISOLanguageName( + // name: "Bislama", + // nativeName: "Bislama", + // ), + // "bs": const ISOLanguageName( + // name: "Bosnian", + // nativeName: "bosanski jezik", + // ), + // "br": const ISOLanguageName( + // name: "Breton", + // nativeName: "brezhoneg", + // ), + // "bg": const ISOLanguageName( + // name: "Bulgarian", + // nativeName: "български език", + // ), + // "my": const ISOLanguageName( + // name: "Burmese", + // nativeName: "ဗမာစာ", + // ), + "ca": const ISOLanguageName( + name: "Catalan", + nativeName: "Català", + ), + // "ch": const ISOLanguageName( + // name: "Chamorro", + // nativeName: "Chamoru", + // ), + // "ce": const ISOLanguageName( + // name: "Chechen", + // nativeName: "нохчийн мотт", + // ), + // "ny": const ISOLanguageName( + // name: "Chichewa", + // nativeName: "chiCheŵa", + // ), + "zh": const ISOLanguageName( + name: "Simplified Chinese", + nativeName: "简体中文", + ), + // "cv": const ISOLanguageName( + // name: "Chuvash", + // nativeName: "чӑваш чӗлхи", + // ), + // "kw": const ISOLanguageName( + // name: "Cornish", + // nativeName: "Kernewek", + // ), + // "co": const ISOLanguageName( + // name: "Corsican", + // nativeName: "lingua corsa", + // ), + // "cr": const ISOLanguageName( + // name: "Cree", + // nativeName: "ᓀᐦᐃᔭᐍᐏᐣ", + // ), + // "hr": const ISOLanguageName( + // name: "Croatian", + // nativeName: "hrvatski", + // ), + "cs": const ISOLanguageName( + name: "Czech", + nativeName: "česky, čeština", + ), + // "da": const ISOLanguageName( + // name: "Danish", + // nativeName: "dansk", + // ), + // "dv": const ISOLanguageName( + // name: "Maldivian;", + // nativeName: "ދިވެހި", + // ), + "nl": const ISOLanguageName( + name: "Dutch", + nativeName: "Nederlands", + ), + "en": const ISOLanguageName( + name: "English", + nativeName: "English", + ), + // "eo": const ISOLanguageName( + // name: "Esperanto", + // nativeName: "Esperanto", + // ), + // "et": const ISOLanguageName( + // name: "Estonian", + // nativeName: "eesti", + // ), + // "ee": const ISOLanguageName( + // name: "Ewe", + // nativeName: "Eʋegbe", + // ), + // "fo": const ISOLanguageName( + // name: "Faroese", + // nativeName: "føroyskt", + // ), + // "fj": const ISOLanguageName( + // name: "Fijian", + // nativeName: "vosa Vakaviti", + // ), + "fi": const ISOLanguageName( + name: "Finnish", + nativeName: "suomi", + ), + "fr": const ISOLanguageName( + name: "French", + nativeName: "français", + ), + // "ff": const ISOLanguageName( + // name: "Fula; Fulah; Pulaar; Pular", + // nativeName: "Fulfulde, Pulaar, Pular", + // ), + // "gl": const ISOLanguageName( + // name: "Galician", + // nativeName: "Galego", + // ), + "ka": const ISOLanguageName( + name: "Georgian", + nativeName: "ქართული", + ), + "de": const ISOLanguageName( + name: "German", + nativeName: "Deutsch", + ), + // "el": const ISOLanguageName( + // name: "Greek, Modern", + // nativeName: "Ελληνικά", + // ), + // "gn": const ISOLanguageName( + // name: "Guaraní", + // nativeName: "Avañeẽ", + // ), + // "gu": const ISOLanguageName( + // name: "Gujarati", + // nativeName: "ગુજરાતી", + // ), + // "ht": const ISOLanguageName( + // name: "Haitian; Haitian Creole", + // nativeName: "Kreyòl ayisyen", + // ), + // "ha": const ISOLanguageName( + // name: "Hausa", + // nativeName: "Hausa, هَوُسَ", + // ), + // "he": const ISOLanguageName( + // name: "Hebrew (modern)", + // nativeName: "עברית", + // ), + // "hz": const ISOLanguageName( + // name: "Herero", + // nativeName: "Otjiherero", + // ), + "hi": const ISOLanguageName( + name: "Hindi", + nativeName: "हिन्दी, हिंदी", + ), + // "ho": const ISOLanguageName( + // name: "Hiri Motu", + // nativeName: "Hiri Motu", + // ), + // "hu": const ISOLanguageName( + // name: "Hungarian", + // nativeName: "Magyar", + // ), + // "ia": const ISOLanguageName( + // name: "Interlingua", + // nativeName: "Interlingua", + // ), + "id": const ISOLanguageName( + name: "Indonesian", + nativeName: "Bahasa Indonesia", + ), + // "ie": const ISOLanguageName( + // name: "Interlingue", + // nativeName: "Occidental", + // ), + // "ga": const ISOLanguageName( + // name: "Irish", + // nativeName: "Gaeilge", + // ), + // "ig": const ISOLanguageName( + // name: "Igbo", + // nativeName: "Asụsụ Igbo", + // ), + // "ik": const ISOLanguageName( + // name: "Inupiaq", + // nativeName: "Iñupiaq, Iñupiatun", + // ), + // "io": const ISOLanguageName( + // name: "Ido", + // nativeName: "Ido", + // ), + // "is": const ISOLanguageName( + // name: "Icelandic", + // nativeName: "Íslenska", + // ), + "it": const ISOLanguageName( + name: "Italian", + nativeName: "Italiano", + ), + // "iu": const ISOLanguageName( + // name: "Inuktitut", + // nativeName: "ᐃᓄᒃᑎᑐᑦ", + // ), + "ja": const ISOLanguageName( + name: "Japanese", + nativeName: "日本語", + ), + // "jv": const ISOLanguageName( + // name: "Javanese", + // nativeName: "basa Jawa", + // ), + // "kl": const ISOLanguageName( + // name: "Kalaallisut, Greenlandic", + // nativeName: "kalaallisut, kalaallit oqaasii", + // ), + // "kn": const ISOLanguageName( + // name: "Kannada", + // nativeName: "ಕನ್ನಡ", + // ), + // "kr": const ISOLanguageName( + // name: "Kanuri", + // nativeName: "Kanuri", + // ), + // "ks": const ISOLanguageName( + // name: "Kashmiri", + // nativeName: "कश्मीरी, كشميري‎", + // ), + // "kk": const ISOLanguageName( + // name: "Kazakh", + // nativeName: "Қазақ тілі", + // ), + // "km": const ISOLanguageName( + // name: "Khmer", + // nativeName: "ភាសាខ្មែរ", + // ), + // "ki": const ISOLanguageName( + // name: "Kikuyu, Gikuyu", + // nativeName: "Gĩkũyũ", + // ), + // "rw": const ISOLanguageName( + // name: "Kinyarwanda", + // nativeName: "Ikinyarwanda", + // ), + // "ky": const ISOLanguageName( + // name: "Kirghiz, Kyrgyz", + // nativeName: "кыргыз тили", + // ), + // "kv": const ISOLanguageName( + // name: "Komi", + // nativeName: "коми кыв", + // ), + // "kg": const ISOLanguageName( + // name: "Kongo", + // nativeName: "KiKongo", + // ), + "ko": const ISOLanguageName( + name: "Korean", + nativeName: "한국어 (韓國語), 조선말 (朝鮮語)", + ), + // "ku": const ISOLanguageName( + // name: "Kurdish", + // nativeName: "Kurdî, كوردی‎", + // ), + // "kj": const ISOLanguageName( + // name: "Kwanyama, Kuanyama", + // nativeName: "Kuanyama", + // ), + // "la": const ISOLanguageName( + // name: "Latin", + // nativeName: "latine, lingua latina", + // ), + // "lb": const ISOLanguageName( + // name: "Luxembourgish, Letzeburgesch", + // nativeName: "Lëtzebuergesch", + // ), + // "lg": const ISOLanguageName( + // name: "Luganda", + // nativeName: "Luganda", + // ), + // "li": const ISOLanguageName( + // name: "Limburgish, Limburgan, Limburger", + // nativeName: "Limburgs", + // ), + // "ln": const ISOLanguageName( + // name: "Lingala", + // nativeName: "Lingála", + // ), + // "lo": const ISOLanguageName( + // name: "Lao", + // nativeName: "ພາສາລາວ", + // ), + // "lt": const ISOLanguageName( + // name: "Lithuanian", + // nativeName: "lietuvių kalba", + // ), + // "lu": const ISOLanguageName( + // name: "Luba-Katanga", + // nativeName: "", + // ), + // "lv": const ISOLanguageName( + // name: "Latvian", + // nativeName: "latviešu valoda", + // ), + // "gv": const ISOLanguageName( + // name: "Manx", + // nativeName: "Gaelg, Gailck", + // ), + // "mk": const ISOLanguageName( + // name: "Macedonian", + // nativeName: "македонски јазик", + // ), + // "mg": const ISOLanguageName( + // name: "Malagasy", + // nativeName: "Malagasy fiteny", + // ), + // "ms": const ISOLanguageName( + // name: "Malay", + // nativeName: "bahasa Melayu, بهاس ملايو‎", + // ), + // "ml": const ISOLanguageName( + // name: "Malayalam", + // nativeName: "മലയാളം", + // ), + // "mt": const ISOLanguageName( + // name: "Maltese", + // nativeName: "Malti", + // ), + // "mi": const ISOLanguageName( + // name: "Māori", + // nativeName: "te reo Māori", + // ), + // "mr": const ISOLanguageName( + // name: "Marathi (Marāṭhī)", + // nativeName: "मराठी", + // ), + // "mh": const ISOLanguageName( + // name: "Marshallese", + // nativeName: "Kajin M̧ajeļ", + // ), + // "mn": const ISOLanguageName( + // name: "Mongolian", + // nativeName: "монгол", + // ), + // "na": const ISOLanguageName( + // name: "Nauru", + // nativeName: "Ekakairũ Naoero", + // ), + // "nv": const ISOLanguageName( + // name: "Navajo, Navaho", + // nativeName: "Diné bizaad, Dinékʼehǰí", + // ), + // "nb": const ISOLanguageName( + // name: "Norwegian Bokmål", + // nativeName: "Norsk bokmål", + // ), + // "nd": const ISOLanguageName( + // name: "North Ndebele", + // nativeName: "isiNdebele", + // ), + "ne": const ISOLanguageName( + name: "Nepali", + nativeName: "नेपाली", + ), + // "ng": const ISOLanguageName( + // name: "Ndonga", + // nativeName: "Owambo", + // ), + // "nn": const ISOLanguageName( + // name: "Norwegian Nynorsk", + // nativeName: "Norsk nynorsk", + // ), + // "no": const ISOLanguageName( + // name: "Norwegian", + // nativeName: "Norsk", + // ), + // "ii": const ISOLanguageName( + // name: "Nuosu", + // nativeName: "ꆈꌠ꒿ Nuosuhxop", + // ), + // "nr": const ISOLanguageName( + // name: "South Ndebele", + // nativeName: "isiNdebele", + // ), + // "oc": const ISOLanguageName( + // name: "Occitan", + // nativeName: "Occitan", + // ), + // "oj": const ISOLanguageName( + // name: "Ojibwe, Ojibwa", + // nativeName: "ᐊᓂᔑᓈᐯᒧᐎᓐ", + // ), + // "cu": const ISOLanguageName( + // name: "Old Church Slavonic", + // nativeName: "ѩзыкъ словѣньскъ", + // ), + // "om": const ISOLanguageName( + // name: "Oromo", + // nativeName: "Afaan Oromoo", + // ), + // "or": const ISOLanguageName( + // name: "Oriya", + // nativeName: "ଓଡ଼ିଆ", + // ), + // "os": const ISOLanguageName( + // name: "Ossetian, Ossetic", + // nativeName: "ирон æвзаг", + // ), + // "pa": const ISOLanguageName( + // name: "Panjabi, Punjabi", + // nativeName: "ਪੰਜਾਬੀ, پنجابی‎", + // ), + // "pi": const ISOLanguageName( + // name: "Pāli", + // nativeName: "पाऴि", + // ), + "fa": const ISOLanguageName( + name: "Persian", + nativeName: "فارسی", + ), + "pl": const ISOLanguageName( + name: "Polish", + nativeName: "polski", + ), + // "ps": const ISOLanguageName( + // name: "Pashto, Pushto", + // nativeName: "پښتو", + // ), + "pt": const ISOLanguageName( + name: "Portuguese", + nativeName: "Português", + ), + // "qu": const ISOLanguageName( + // name: "Quechua", + // nativeName: "Runa Simi, Kichwa", + // ), + // "rm": const ISOLanguageName( + // name: "Romansh", + // nativeName: "rumantsch grischun", + // ), + // "rn": const ISOLanguageName( + // name: "Kirundi", + // nativeName: "kiRundi", + // ), + // "ro": const ISOLanguageName( + // name: "Romanian, Moldavian, Moldovan", + // nativeName: "română", + // ), + "ru": const ISOLanguageName( + name: "Russian", + nativeName: "русский язык", + ), + // "sa": const ISOLanguageName( + // name: "Sanskrit (Saṁskṛta)", + // nativeName: "संस्कृतम्", + // ), + // "sc": const ISOLanguageName( + // name: "Sardinian", + // nativeName: "sardu", + // ), + // "sd": const ISOLanguageName( + // name: "Sindhi", + // nativeName: "सिन्धी, سنڌي، سندھی‎", + // ), + // "se": const ISOLanguageName( + // name: "Northern Sami", + // nativeName: "Davvisámegiella", + // ), + // "sm": const ISOLanguageName( + // name: "Samoan", + // nativeName: "gagana faa Samoa", + // ), + // "sg": const ISOLanguageName( + // name: "Sango", + // nativeName: "yângâ tî sängö", + // ), + // "sr": const ISOLanguageName( + // name: "Serbian", + // nativeName: "српски језик", + // ), + // "gd": const ISOLanguageName( + // name: "Scottish Gaelic; Gaelic", + // nativeName: "Gàidhlig", + // ), + // "sn": const ISOLanguageName( + // name: "Shona", + // nativeName: "chiShona", + // ), + // "si": const ISOLanguageName( + // name: "Sinhala, Sinhalese", + // nativeName: "සිංහල", + // ), + // "sk": const ISOLanguageName( + // name: "Slovak", + // nativeName: "slovenčina", + // ), + // "sl": const ISOLanguageName( + // name: "Slovene", + // nativeName: "slovenščina", + // ), + // "so": const ISOLanguageName( + // name: "Somali", + // nativeName: "Soomaaliga, af Soomaali", + // ), + // "st": const ISOLanguageName( + // name: "Southern Sotho", + // nativeName: "Sesotho", + // ), + "es": const ISOLanguageName( + name: "Spanish", + nativeName: "español", + ), + // "su": const ISOLanguageName( + // name: "Sundanese", + // nativeName: "Basa Sunda", + // ), + // "sw": const ISOLanguageName( + // name: "Swahili", + // nativeName: "Kiswahili", + // ), + // "ss": const ISOLanguageName( + // name: "Swati", + // nativeName: "SiSwati", + // ), + // "sv": const ISOLanguageName( + // name: "Swedish", + // nativeName: "svenska", + // ), + // "ta": const ISOLanguageName( + // name: "Tamil", + // nativeName: "தமிழ்", + // ), + // "te": const ISOLanguageName( + // name: "Telugu", + // nativeName: "తెలుగు", + // ), + // "tg": const ISOLanguageName( + // name: "Tajik", + // nativeName: "тоҷикӣ, toğikī, تاجیکی‎", + // ), + "th": const ISOLanguageName( + name: "Thai", + nativeName: "ไทย", + ), + // "ti": const ISOLanguageName( + // name: "Tigrinya", + // nativeName: "ትግርኛ", + // ), + // "bo": const ISOLanguageName( + // name: "Tibetan Standard, Tibetan, Central", + // nativeName: "བོད་ཡིག", + // ), + // "tk": const ISOLanguageName( + // name: "Turkmen", + // nativeName: "Türkmen, Түркмен", + // ), + // "tl": const ISOLanguageName( + // name: "Tagalog", + // nativeName: "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔", + // ), + // "tn": const ISOLanguageName( + // name: "Tswana", + // nativeName: "Setswana", + // ), + // "to": const ISOLanguageName( + // name: "Tonga (Tonga Islands)", + // nativeName: "faka Tonga", + // ), + "tr": const ISOLanguageName( + name: "Turkish", + nativeName: "Türkçe", + ), + // "ts": const ISOLanguageName( + // name: "Tsonga", + // nativeName: "Xitsonga", + // ), + // "tt": const ISOLanguageName( + // name: "Tatar", + // nativeName: "татарча, tatarça, تاتارچا‎", + // ), + // "tw": const ISOLanguageName( + // name: "Twi", + // nativeName: "Twi", + // ), + // "ty": const ISOLanguageName( + // name: "Tahitian", + // nativeName: "Reo Tahiti", + // ), + // "ug": const ISOLanguageName( + // name: "Uighur, Uyghur", + // nativeName: "Uyƣurqə, ئۇيغۇرچە‎", + // ), + "uk": const ISOLanguageName( + name: "Ukrainian", + nativeName: "українська", + ), + // "ur": const ISOLanguageName( + // name: "Urdu", + // nativeName: "اردو", + // ), + // "uz": const ISOLanguageName( + // name: "Uzbek", + // nativeName: "zbek, Ўзбек, أۇزبېك‎", + // ), + // "ve": const ISOLanguageName( + // name: "Venda", + // nativeName: "Tshivenḓa", + // ), + "vi": const ISOLanguageName( + name: "Vietnamese", + nativeName: "Tiếng Việt", + ), + // "vo": const ISOLanguageName( + // name: "Volapük", + // nativeName: "Volapük", + // ), + // "wa": const ISOLanguageName( + // name: "Walloon", + // nativeName: "Walon", + // ), + // "cy": const ISOLanguageName( + // name: "Welsh", + // nativeName: "Cymraeg", + // ), + // "wo": const ISOLanguageName( + // name: "Wolof", + // nativeName: "Wollof", + // ), + // "fy": const ISOLanguageName( + // name: "Western Frisian", + // nativeName: "Frysk", + // ), + // "xh": const ISOLanguageName( + // name: "Xhosa", + // nativeName: "isiXhosa", + // ), + // "yi": const ISOLanguageName( + // name: "Yiddish", + // nativeName: "ייִדיש", + // ), + // "yo": const ISOLanguageName( + // name: "Yoruba", + // nativeName: "Yorùbá", + // ), + // "za": const ISOLanguageName( + // name: "Zhuang, Chuang", + // nativeName: "Saɯ cueŋƅ, Saw cuengh", + // ) + }; + + static ISOLanguageName getDisplayLanguage(key) { + if (isoLangs.containsKey(key)) { + return isoLangs[key]!; + } else { + throw Exception("Language key incorrect"); + } + } +} diff --git a/lib/collections/spotify_markets.dart b/lib/collections/spotify_markets.dart new file mode 100755 index 0000000..514b3f0 --- /dev/null +++ b/lib/collections/spotify_markets.dart @@ -0,0 +1,189 @@ +// Country Codes contributed by momobobe + +import 'package:spotify/spotify.dart'; + +final spotifyMarkets = [ + (Market.AL, "Albania (AL)"), + (Market.DZ, "Algeria (DZ)"), + (Market.AD, "Andorra (AD)"), + (Market.AO, "Angola (AO)"), + (Market.AG, "Antigua and Barbuda (AG)"), + (Market.AR, "Argentina (AR)"), + (Market.AM, "Armenia (AM)"), + (Market.AU, "Australia (AU)"), + (Market.AT, "Austria (AT)"), + (Market.AZ, "Azerbaijan (AZ)"), + (Market.BH, "Bahrain (BH)"), + (Market.BD, "Bangladesh (BD)"), + (Market.BB, "Barbados (BB)"), + (Market.BY, "Belarus (BY)"), + (Market.BE, "Belgium (BE)"), + (Market.BZ, "Belize (BZ)"), + (Market.BJ, "Benin (BJ)"), + (Market.BT, "Bhutan (BT)"), + (Market.BO, "Bolivia (BO)"), + (Market.BA, "Bosnia and Herzegovina (BA)"), + (Market.BW, "Botswana (BW)"), + (Market.BR, "Brazil (BR)"), + (Market.BN, "Brunei Darussalam (BN)"), + (Market.BG, "Bulgaria (BG)"), + (Market.BF, "Burkina Faso (BF)"), + (Market.BI, "Burundi (BI)"), + (Market.CV, "Cabo Verde / Cape Verde (CV)"), + (Market.KH, "Cambodia (KH)"), + (Market.CM, "Cameroon (CM)"), + (Market.CA, "Canada (CA)"), + (Market.TD, "Chad (TD)"), + (Market.CL, "Chile (CL)"), + (Market.CO, "Colombia (CO)"), + (Market.KM, "Comoros (KM)"), + (Market.CR, "Costa Rica (CR)"), + (Market.HR, "Croatia (HR)"), + (Market.CW, "Curaçao (CW)"), + (Market.CY, "Cyprus (CY)"), + (Market.CZ, "Czech Republic (CZ)"), + (Market.CI, "Ivory Coast (CI)"), + (Market.CD, "Congo (CD)"), + (Market.DK, "Denmark (DK)"), + (Market.DJ, "Djibouti (DJ)"), + (Market.DM, "Dominica (DM)"), + (Market.DO, "Dominican Republic (DO)"), + (Market.EC, "Ecuador (EC)"), + (Market.EG, "Egypt (EG)"), + (Market.SV, "El Salvador (SV)"), + (Market.GQ, "Equatorial Guinea (GQ)"), + (Market.EE, "Estonia (EE)"), + (Market.SZ, "Eswatini (SZ)"), + (Market.FJ, "Fiji (FJ)"), + (Market.FI, "Finland (FI)"), + (Market.FR, "France (FR)"), + (Market.GA, "Gabon (GA)"), + (Market.GE, "Georgia (GE)"), + (Market.DE, "Germany (DE)"), + (Market.GH, "Ghana (GH)"), + (Market.GR, "Greece (GR)"), + (Market.GD, "Grenada (GD)"), + (Market.GT, "Guatemala (GT)"), + (Market.GN, "Guinea (GN)"), + (Market.GW, "Guinea-Bissau (GW)"), + (Market.GY, "Guyana (GY)"), + (Market.HT, "Haiti (HT)"), + (Market.HN, "Honduras (HN)"), + (Market.HK, "Hong Kong (HK)"), + (Market.HU, "Hungary (HU)"), + (Market.IS, "Iceland (IS)"), + (Market.IN, "India (IN)"), + (Market.ID, "Indonesia (ID)"), + (Market.IQ, "Iraq (IQ)"), + (Market.IE, "Ireland (IE)"), + (Market.IL, "Israel (IL)"), + (Market.IT, "Italy (IT)"), + (Market.JM, "Jamaica (JM)"), + (Market.JP, "Japan (JP)"), + (Market.JO, "Jordan (JO)"), + (Market.KZ, "Kazakhstan (KZ)"), + (Market.KE, "Kenya (KE)"), + (Market.KI, "Kiribati (KI)"), + (Market.XK, "Kosovo (XK)"), + (Market.KW, "Kuwait (KW)"), + (Market.KG, "Kyrgyzstan (KG)"), + (Market.LA, "Laos (LA)"), + (Market.LV, "Latvia (LV)"), + (Market.LB, "Lebanon (LB)"), + (Market.LS, "Lesotho (LS)"), + (Market.LR, "Liberia (LR)"), + (Market.LY, "Libya (LY)"), + (Market.LI, "Liechtenstein (LI)"), + (Market.LT, "Lithuania (LT)"), + (Market.LU, "Luxembourg (LU)"), + (Market.MO, "Macao / Macau (MO)"), + (Market.MG, "Madagascar (MG)"), + (Market.MW, "Malawi (MW)"), + (Market.MY, "Malaysia (MY)"), + (Market.MV, "Maldives (MV)"), + (Market.ML, "Mali (ML)"), + (Market.MT, "Malta (MT)"), + (Market.MH, "Marshall Islands (MH)"), + (Market.MR, "Mauritania (MR)"), + (Market.MU, "Mauritius (MU)"), + (Market.MX, "Mexico (MX)"), + (Market.FM, "Micronesia (FM)"), + (Market.MD, "Moldova (MD)"), + (Market.MC, "Monaco (MC)"), + (Market.MN, "Mongolia (MN)"), + (Market.ME, "Montenegro (ME)"), + (Market.MA, "Morocco (MA)"), + (Market.MZ, "Mozambique (MZ)"), + (Market.NA, "Namibia (NA)"), + (Market.NR, "Nauru (NR)"), + (Market.NP, "Nepal (NP)"), + (Market.NL, "Netherlands (NL)"), + (Market.NZ, "New Zealand (NZ)"), + (Market.NI, "Nicaragua (NI)"), + (Market.NE, "Niger (NE)"), + (Market.NG, "Nigeria (NG)"), + (Market.MK, "North Macedonia (MK)"), + (Market.NO, "Norway (NO)"), + (Market.OM, "Oman (OM)"), + (Market.PK, "Pakistan (PK)"), + (Market.PW, "Palau (PW)"), + (Market.PS, "Palestine (PS)"), + (Market.PA, "Panama (PA)"), + (Market.PG, "Papua New Guinea (PG)"), + (Market.PY, "Paraguay (PY)"), + (Market.PE, "Peru (PE)"), + (Market.PH, "Philippines (PH)"), + (Market.PL, "Poland (PL)"), + (Market.PT, "Portugal (PT)"), + (Market.QA, "Qatar (QA)"), + (Market.CG, "Congo (CG)"), + (Market.RO, "Romania (RO)"), + (Market.RU, "Russia (RU)"), + (Market.RW, "Rwanda (RW)"), + (Market.WS, "Samoa (WS)"), + (Market.SM, "San Marino (SM)"), + (Market.SA, "Saudi Arabia (SA)"), + (Market.SN, "Senegal (SN)"), + (Market.RS, "Serbia (RS)"), + (Market.SC, "Seychelles (SC)"), + (Market.SL, "Sierra Leone (SL)"), + (Market.SG, "Singapore (SG)"), + (Market.SK, "Slovakia (SK)"), + (Market.SI, "Slovenia (SI)"), + (Market.SB, "Solomon Islands (SB)"), + (Market.ZA, "South Africa (ZA)"), + (Market.KR, "South Korea (KR)"), + (Market.ES, "Spain (ES)"), + (Market.LK, "Sri Lanka (LK)"), + (Market.KN, "St. Kitts and Nevis (KN)"), + (Market.LC, "St. Lucia (LC)"), + (Market.SR, "Suriname (SR)"), + (Market.SE, "Sweden (SE)"), + (Market.CH, "Switzerland (CH)"), + (Market.ST, "São Tomé and Príncipe (ST)"), + (Market.TW, "Taiwan (TW)"), + (Market.TJ, "Tajikistan (TJ)"), + (Market.TZ, "Tanzania (TZ)"), + (Market.TH, "Thailand (TH)"), + (Market.BS, "The Bahamas (BS)"), + (Market.GM, "The Gambia (GM)"), + (Market.TL, "East Timor (TL)"), + (Market.TG, "Togo (TG)"), + (Market.TO, "Tonga (TO)"), + (Market.TT, "Trinidad and Tobago (TT)"), + (Market.TN, "Tunisia (TN)"), + (Market.TR, "Turkey (TR)"), + (Market.TV, "Tuvalu (TV)"), + (Market.UG, "Uganda (UG)"), + (Market.UA, "Ukraine (UA)"), + (Market.AE, "United Arab Emirates (AE)"), + (Market.GB, "United Kingdom (GB)"), + (Market.US, "United States (US)"), + (Market.UY, "Uruguay (UY)"), + (Market.UZ, "Uzbekistan (UZ)"), + (Market.VU, "Vanuatu (VU)"), + (Market.VE, "Venezuela (VE)"), + (Market.VN, "Vietnam (VN)"), + (Market.ZM, "Zambia (ZM)"), + (Market.ZW, "Zimbabwe (ZW)"), +]; diff --git a/lib/main.dart b/lib/main.dart index be773a7..48fa43f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,18 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/router.dart'; +import 'package:rhythm_box/services/server/active_sourced_track.dart'; +import 'package:rhythm_box/services/server/routes/playback.dart'; +import 'package:rhythm_box/services/server/server.dart'; +import 'package:rhythm_box/services/server/sourced_track.dart'; import 'package:rhythm_box/translations.dart'; void main() { + MediaKit.ensureInitialized(); + runApp(const MyApp()); } @@ -42,5 +50,10 @@ class MyApp extends StatelessWidget { void _initializeProviders(BuildContext context) async { Get.lazyPut(() => SpotifyProvider()); + Get.lazyPut(() => AudioPlayerProvider()); + Get.lazyPut(() => ActiveSourcedTrackProvider()); + Get.lazyPut(() => SourcedTrackProvider()); + Get.lazyPut(() => ServerPlaybackRoutesProvider()); + Get.lazyPut(() => PlaybackServerProvider()); } } diff --git a/lib/providers/audio_player.dart b/lib/providers/audio_player.dart new file mode 100644 index 0000000..79b4231 --- /dev/null +++ b/lib/providers/audio_player.dart @@ -0,0 +1,122 @@ +import 'dart:math'; + +import 'package:get/get.dart'; +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:rhythm_box/services/audio_player/state.dart'; +import 'package:rhythm_box/services/local_track.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; +import 'package:spotify/spotify.dart' hide Playlist; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AudioPlayerProvider extends GetxController { + late final SharedPreferences _prefs; + + Rx state = Rx(AudioPlayerState( + playing: false, + shuffled: false, + loopMode: PlaylistMode.none, + playlist: const Playlist([]), + collections: [], + )); + + AudioPlayerProvider() { + SharedPreferences.getInstance().then((ins) { + _prefs = ins; + }); + } + + Future _syncSavedState() async { + final data = _prefs.getBool("player_state"); + if (data == null) return; + + // TODO Serilize and deserilize this state + + // TODO Sync saved playlist + } + + Future load( + List tracks, { + int initialIndex = 0, + bool autoPlay = false, + }) async { + final medias = tracks.map((x) => RhythmMedia(x)).toList(); + + // Giving the initial track a boost so MediaKit won't skip + // because of timeout + final intendedActiveTrack = medias.elementAt(initialIndex); + if (intendedActiveTrack.track is! LocalTrack) { + await SourcedTrack.fetchFromTrack(track: intendedActiveTrack.track); + } + + if (medias.isEmpty) return; + + await audioPlayer.openPlaylist( + medias.map((s) => s as Media).toList(), + initialIndex: initialIndex, + autoPlay: autoPlay, + ); + } + + Future addTracksAtFirst(Iterable tracks) async { + if (state.value.tracks.length == 1) { + return addTracks(tracks); + } + + for (int i = 0; i < tracks.length; i++) { + final track = tracks.elementAt(i); + + await audioPlayer.addTrackAt( + RhythmMedia(track), + max(state.value.playlist.index, 0) + i + 1, + ); + } + } + + Future addTrack(Track track) async { + await audioPlayer.addTrack(RhythmMedia(track)); + } + + Future addTracks(Iterable tracks) async { + for (final track in tracks) { + await audioPlayer.addTrack(RhythmMedia(track)); + } + } + + Future removeTrack(String trackId) async { + final index = + state.value.tracks.indexWhere((element) => element.id == trackId); + + if (index == -1) return; + + await audioPlayer.removeTrack(index); + } + + Future removeTracks(Iterable trackIds) async { + for (final trackId in trackIds) { + await removeTrack(trackId); + } + } + + Future jumpToTrack(Track track) async { + final index = state.value.tracks + .toList() + .indexWhere((element) => element.id == track.id); + if (index == -1) return; + await audioPlayer.jumpTo(index); + } + + Future moveTrack(int oldIndex, int newIndex) async { + if (oldIndex == newIndex || + newIndex < 0 || + oldIndex < 0 || + newIndex > state.value.tracks.length - 1 || + oldIndex > state.value.tracks.length - 1) return; + + await audioPlayer.moveTrack(oldIndex, newIndex); + } + + Future stop() async { + await audioPlayer.stop(); + } +} diff --git a/lib/providers/piped.dart b/lib/providers/piped.dart deleted file mode 100644 index 3005b0f..0000000 --- a/lib/providers/piped.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:get/get.dart'; - -class PipedProvider extends GetxController {} diff --git a/lib/router.dart b/lib/router.dart index 025e2bd..fb079d1 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -13,13 +13,6 @@ final router = GoRouter(routes: [ name: "explore", builder: (context, state) => const ExploreScreen(), ), - GoRoute( - path: "/playlist/:id", - name: "playlistView", - builder: (context, state) => PlaylistViewScreen( - playlistId: state.pathParameters['id']!, - ), - ), GoRoute( path: "/settings", name: "settings", @@ -27,4 +20,11 @@ final router = GoRouter(routes: [ ), ], ), + GoRoute( + path: "/playlist/:id", + name: "playlistView", + builder: (context, state) => PlaylistViewScreen( + playlistId: state.pathParameters['id']!, + ), + ), ]); diff --git a/lib/services/artist.dart b/lib/services/artist.dart new file mode 100644 index 0000000..7997355 --- /dev/null +++ b/lib/services/artist.dart @@ -0,0 +1,7 @@ +import 'package:spotify/spotify.dart'; + +extension ArtistExtension on List { + String asString() { + return map((e) => e.name?.replaceAll(",", " ")).join(", "); + } +} diff --git a/lib/services/audio_player/state.dart b/lib/services/audio_player/state.dart new file mode 100644 index 0000000..6c1251d --- /dev/null +++ b/lib/services/audio_player/state.dart @@ -0,0 +1,108 @@ +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; + +class AudioPlayerState { + final bool playing; + final PlaylistMode loopMode; + final bool shuffled; + final Playlist playlist; + + final List tracks; + final List collections; + + AudioPlayerState({ + required this.playing, + required this.loopMode, + required this.shuffled, + required this.playlist, + required this.collections, + List? tracks, + }) : tracks = tracks ?? + playlist.medias + .map((media) => RhythmMedia.fromMedia(media).track) + .toList(); + + factory AudioPlayerState.fromJson(Map json) { + return AudioPlayerState( + playing: json['playing'], + loopMode: PlaylistMode.values.firstWhere( + (e) => e.name == json['loopMode'], + orElse: () => audioPlayer.loopMode, + ), + shuffled: json['shuffled'], + playlist: Playlist( + json['playlist']['medias'] + .map( + (media) => RhythmMedia.fromMedia(Media( + media['uri'], + extras: media['extras'], + httpHeaders: media['httpHeaders'], + )), + ) + .cast() + .toList(), + index: json['playlist']['index'], + ), + collections: List.from(json['collections']), + ); + } + + Map toJson() { + return { + 'playing': playing, + 'loopMode': loopMode.name, + 'shuffled': shuffled, + 'playlist': { + 'medias': playlist.medias + .map((media) => { + 'uri': media.uri, + 'extras': media.extras, + 'httpHeaders': media.httpHeaders, + }) + .toList(), + 'index': playlist.index, + }, + 'collections': collections, + }; + } + + AudioPlayerState copyWith({ + bool? playing, + PlaylistMode? loopMode, + bool? shuffled, + Playlist? playlist, + List? collections, + }) { + return AudioPlayerState( + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + playlist: playlist ?? this.playlist, + collections: collections ?? this.collections, + tracks: playlist == null ? tracks : null, + ); + } + + Track? get activeTrack { + if (playlist.index == -1) return null; + return tracks.elementAtOrNull(playlist.index); + } + + Media? get activeMedia { + if (playlist.index == -1 || playlist.medias.isEmpty) return null; + return playlist.medias.elementAt(playlist.index); + } + + bool containsTrack(Track track) { + return tracks.any((t) => t.id == track.id); + } + + bool containsTracks(List tracks) { + return tracks.every(containsTrack); + } + + bool containsCollection(String collectionId) { + return collections.contains(collectionId); + } +} diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart new file mode 100755 index 0000000..7ca20a3 --- /dev/null +++ b/lib/services/audio_services/audio_services.dart @@ -0,0 +1,84 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:rhythm_box/platform.dart'; +import 'package:rhythm_box/services/audio_services/image.dart'; +import 'package:spotify/spotify.dart'; +import 'package:rhythm_box/services/audio_services/mobile_audio_service.dart'; +import 'package:rhythm_box/services/audio_services/windows_audio_service.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; +import 'package:rhythm_box/services/artist.dart'; + +class AudioServices with WidgetsBindingObserver { + final MobileAudioService? mobile; + final WindowsAudioService? smtc; + + AudioServices(this.mobile, this.smtc) { + WidgetsBinding.instance.addObserver(this); + } + + static Future create() async { + final mobile = + PlatformInfo.isMobile || PlatformInfo.isMacOS || PlatformInfo.isLinux + ? await AudioService.init( + builder: () => MobileAudioService(), + config: AudioServiceConfig( + androidNotificationChannelId: PlatformInfo.isLinux + ? 'RhythmBox' + : 'dev.solsynth.rhythmBox', + androidNotificationChannelName: 'RhythmBox', + androidNotificationOngoing: false, + androidNotificationIcon: "drawable/ic_launcher_monochrome", + androidStopForegroundOnPause: false, + androidNotificationChannelDescription: "RhythmBox Music", + ), + ) + : null; + final smtc = PlatformInfo.isWindows ? WindowsAudioService() : null; + + return AudioServices(mobile, smtc); + } + + Future addTrack(Track track) async { + await smtc?.addTrack(track); + mobile?.addItem(MediaItem( + id: track.id!, + album: track.album?.name ?? "", + title: track.name!, + artist: (track.artists)?.asString() ?? "", + duration: track is SourcedTrack + ? track.sourceInfo.duration + : Duration(milliseconds: track.durationMs ?? 0), + artUri: Uri.parse( + (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + ), + playable: true, + )); + } + + void activateSession() { + mobile?.session?.setActive(true); + } + + void deactivateSession() { + mobile?.session?.setActive(false); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.detached: + deactivateSession(); + mobile?.stop(); + break; + default: + break; + } + } + + void dispose() { + smtc?.dispose(); + WidgetsBinding.instance.removeObserver(this); + } +} diff --git a/lib/services/audio_services/image.dart b/lib/services/audio_services/image.dart new file mode 100644 index 0000000..7a34b38 --- /dev/null +++ b/lib/services/audio_services/image.dart @@ -0,0 +1,34 @@ +import 'package:rhythm_box/services/primitive.dart'; +import 'package:spotify/spotify.dart'; +import 'package:rhythm_box/collections/assets.gen.dart'; +import 'package:collection/collection.dart'; + +enum ImagePlaceholder { + albumArt, + artist, + collection, + online, +} + +extension SpotifyImageExtensions on List? { + String asUrlString({ + int index = 1, + required ImagePlaceholder placeholder, + }) { + final String placeholderUrl = { + ImagePlaceholder.albumArt: Assets.albumPlaceholder.path, + ImagePlaceholder.artist: Assets.userPlaceholder.path, + ImagePlaceholder.collection: Assets.placeholder.path, + ImagePlaceholder.online: + "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png", + }[placeholder]!; + + final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!)); + + return sortedImage != null && sortedImage.isNotEmpty + ? sortedImage[ + index > sortedImage.length - 1 ? sortedImage.length - 1 : index] + .url! + : placeholderUrl; + } +} diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart new file mode 100755 index 0000000..24fd362 --- /dev/null +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -0,0 +1,153 @@ +import 'dart:async'; + +import 'package:audio_service/audio_service.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:get/get.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:rhythm_box/services/audio_player/state.dart'; + +class MobileAudioService extends BaseAudioHandler { + AudioSession? session; + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + AudioPlayerState get playlist => Get.find().state.value; + + MobileAudioService() { + AudioSession.instance.then((s) { + session = s; + session?.configure(const AudioSessionConfiguration.music()); + + bool wasPausedByBeginEvent = false; + + s.interruptionEventStream.listen((event) async { + if (event.begin) { + switch (event.type) { + case AudioInterruptionType.duck: + await audioPlayer.setVolume(0.5); + break; + case AudioInterruptionType.pause: + case AudioInterruptionType.unknown: + { + wasPausedByBeginEvent = audioPlayer.isPlaying; + await audioPlayer.pause(); + break; + } + } + } else { + switch (event.type) { + case AudioInterruptionType.duck: + await audioPlayer.setVolume(1.0); + break; + case AudioInterruptionType.pause when wasPausedByBeginEvent: + case AudioInterruptionType.unknown when wasPausedByBeginEvent: + await audioPlayer.resume(); + wasPausedByBeginEvent = false; + break; + default: + break; + } + } + }); + + s.becomingNoisyEventStream.listen((_) { + audioPlayer.pause(); + }); + }); + audioPlayer.playerStateStream.listen((state) async { + playbackState.add(await _transformEvent()); + }); + + audioPlayer.positionStream.listen((pos) async { + playbackState.add(await _transformEvent()); + }); + audioPlayer.bufferedPositionStream.listen((pos) async { + playbackState.add(await _transformEvent()); + }); + } + + void addItem(MediaItem item) { + session?.setActive(true); + mediaItem.add(item); + } + + @override + Future play() => audioPlayer.resume(); + + @override + Future pause() => audioPlayer.pause(); + + @override + Future seek(Duration position) => audioPlayer.seek(position); + + @override + Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { + await super.setShuffleMode(shuffleMode); + + audioPlayer.setShuffle(shuffleMode == AudioServiceShuffleMode.all); + } + + @override + Future setRepeatMode(AudioServiceRepeatMode repeatMode) async { + super.setRepeatMode(repeatMode); + audioPlayer.setLoopMode(switch (repeatMode) { + AudioServiceRepeatMode.all || + AudioServiceRepeatMode.group => + PlaylistMode.loop, + AudioServiceRepeatMode.one => PlaylistMode.single, + _ => PlaylistMode.none, + }); + } + + @override + Future stop() async { + await Get.find().stop(); + } + + @override + Future skipToNext() async { + await audioPlayer.skipToNext(); + await super.skipToNext(); + } + + @override + Future skipToPrevious() async { + await audioPlayer.skipToPrevious(); + await super.skipToPrevious(); + } + + @override + Future onTaskRemoved() async { + await Get.find().stop(); + return super.onTaskRemoved(); + } + + Future _transformEvent() async { + return PlaybackState( + controls: [ + MediaControl.skipToPrevious, + audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play, + MediaControl.skipToNext, + MediaControl.stop, + ], + systemActions: { + MediaAction.seek, + }, + androidCompactActionIndices: const [0, 1, 2], + playing: audioPlayer.isPlaying, + updatePosition: audioPlayer.position, + bufferedPosition: audioPlayer.bufferedPosition, + shuffleMode: audioPlayer.isShuffled == true + ? AudioServiceShuffleMode.all + : AudioServiceShuffleMode.none, + repeatMode: switch (audioPlayer.loopMode) { + PlaylistMode.loop => AudioServiceRepeatMode.all, + PlaylistMode.single => AudioServiceRepeatMode.one, + _ => AudioServiceRepeatMode.none, + }, + processingState: audioPlayer.isBuffering + ? AudioProcessingState.loading + : AudioProcessingState.ready, + ); + } +} diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart new file mode 100755 index 0000000..062304a --- /dev/null +++ b/lib/services/audio_services/windows_audio_service.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:get/get.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/services/audio_services/image.dart'; +import 'package:smtc_windows/smtc_windows.dart'; +import 'package:spotify/spotify.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:rhythm_box/services/audio_player/playback_state.dart'; +import 'package:rhythm_box/services/artist.dart'; + +class WindowsAudioService { + final SMTCWindows smtc; + + final subscriptions = []; + + WindowsAudioService() : smtc = SMTCWindows(enabled: false) { + smtc.setPlaybackStatus(PlaybackStatus.Stopped); + final buttonStream = smtc.buttonPressStream.listen((event) { + switch (event) { + case PressedButton.play: + audioPlayer.resume(); + break; + case PressedButton.pause: + audioPlayer.pause(); + break; + case PressedButton.next: + audioPlayer.skipToNext(); + break; + case PressedButton.previous: + audioPlayer.skipToPrevious(); + break; + case PressedButton.stop: + Get.find().stop(); + break; + default: + break; + } + }); + + final playerStateStream = + audioPlayer.playerStateStream.listen((state) async { + switch (state) { + case AudioPlaybackState.playing: + await smtc.setPlaybackStatus(PlaybackStatus.Playing); + break; + case AudioPlaybackState.paused: + await smtc.setPlaybackStatus(PlaybackStatus.Paused); + break; + case AudioPlaybackState.stopped: + await smtc.setPlaybackStatus(PlaybackStatus.Stopped); + break; + case AudioPlaybackState.completed: + await smtc.setPlaybackStatus(PlaybackStatus.Changing); + break; + default: + break; + } + }); + + final positionStream = audioPlayer.positionStream.listen((pos) async { + await smtc.setPosition(pos); + }); + + final durationStream = audioPlayer.durationStream.listen((duration) async { + await smtc.setEndTime(duration); + }); + + subscriptions.addAll([ + buttonStream, + playerStateStream, + positionStream, + durationStream, + ]); + } + + Future addTrack(Track track) async { + if (!smtc.enabled) { + await smtc.enableSmtc(); + } + await smtc.updateMetadata( + MusicMetadata( + title: track.name!, + albumArtist: track.artists?.firstOrNull?.name ?? "Unknown", + artist: track.artists?.asString() ?? "Unknown", + album: track.album?.name ?? "Unknown", + thumbnail: (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + ), + ); + } + + void dispose() { + smtc.disableSmtc(); + smtc.dispose(); + for (var element in subscriptions) { + element.cancel(); + } + } +} diff --git a/lib/services/primitive.dart b/lib/services/primitive.dart new file mode 100755 index 0000000..801c2e5 --- /dev/null +++ b/lib/services/primitive.dart @@ -0,0 +1,53 @@ +import 'dart:math'; +import 'package:uuid/uuid.dart'; + +abstract class PrimitiveUtils { + static bool containsTextInBracket(String matcher, String text) { + final allMatches = RegExp(r"(?<=\().+?(?=\))").allMatches(matcher); + if (allMatches.isEmpty) return false; + return allMatches + .map((e) => e.group(0)) + .every((match) => match?.contains(text) ?? false); + } + + static final Random _random = Random(); + static T getRandomElement(List list) { + return list[_random.nextInt(list.length)]; + } + + static const uuid = Uuid(); + + static String toReadableNumber(double num) { + if (num > 999 && num < 99999) { + return "${(num / 1000).toStringAsFixed(0)}K"; + } else if (num > 99999 && num < 999999) { + return "${(num / 1000).toStringAsFixed(0)}K"; + } else if (num > 999999 && num < 999999999) { + return "${(num / 1000000).toStringAsFixed(0)}M"; + } else if (num > 999999999) { + return "${(num / 1000000000).toStringAsFixed(0)}B"; + } else { + return num.toStringAsFixed(0); + } + } + + static Future raceMultiple( + Future Function() inner, { + Duration timeout = const Duration(milliseconds: 2500), + int retryCount = 4, + }) async { + return Future.any( + List.generate(retryCount, (i) { + if (i == 0) return inner(); + return Future.delayed( + Duration(milliseconds: timeout.inMilliseconds * i), + inner, + ); + }), + ); + } + + static String toSafeFileName(String str) { + return str.replaceAll(RegExp(r'[/\?%*:|"<>]'), ' '); + } +} diff --git a/lib/services/server/active_sourced_track.dart b/lib/services/server/active_sourced_track.dart new file mode 100755 index 0000000..2595c2e --- /dev/null +++ b/lib/services/server/active_sourced_track.dart @@ -0,0 +1,39 @@ +import 'package:get/get.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; + +class ActiveSourcedTrackProvider extends GetxController { + Rx state = Rx(null); + + void updateTrack(SourcedTrack? sourcedTrack) { + state.value = sourcedTrack; + } + + Future populateSibling() async { + if (state.value == null) return; + state.value = await state.value!.copyWithSibling(); + } + + Future swapSibling(SourceInfo sibling) async { + if (state.value == null) return; + await populateSibling(); + final newTrack = await state.value!.swapWithSibling(sibling); + if (newTrack == null) return; + + state.value = newTrack; + await audioPlayer.pause(); + + final playback = Get.find(); + final oldActiveIndex = audioPlayer.currentIndex; + + await playback.addTracksAtFirst([newTrack]); + await Future.delayed(const Duration(milliseconds: 50)); + await playback.jumpToTrack(newTrack); + + await audioPlayer.removeTrack(oldActiveIndex); + + await audioPlayer.resume(); + } +} diff --git a/lib/services/server/routes/playback.dart b/lib/services/server/routes/playback.dart new file mode 100755 index 0000000..fa3e03f --- /dev/null +++ b/lib/services/server/routes/playback.dart @@ -0,0 +1,66 @@ +import 'dart:developer'; + +import 'package:dio/dio.dart' hide Response; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart' hide Response; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:rhythm_box/services/server/active_sourced_track.dart'; +import 'package:rhythm_box/services/server/sourced_track.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; +import 'package:shelf/shelf.dart'; + +class ServerPlaybackRoutesProvider { + /// @get('/stream/') + Future getStreamTrackId(Request request, String trackId) async { + final AudioPlayerProvider playback = Get.find(); + + try { + final track = playback.state.value.tracks + .firstWhere((element) => element.id == trackId); + + final ActiveSourcedTrackProvider activeSourcedTrack = Get.find(); + final sourcedTrack = activeSourcedTrack.state.value?.id == track.id + ? activeSourcedTrack + : await Get.find().fetch(RhythmMedia(track)); + + activeSourcedTrack.updateTrack(sourcedTrack as SourcedTrack?); + + final res = await Dio().get( + sourcedTrack!.url, + options: Options( + headers: { + ...request.headers, + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "host": Uri.parse(sourcedTrack.url).host, + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + }, + responseType: ResponseType.stream, + validateStatus: (status) => status! < 500, + ), + ); + + final audioStream = + (res.data?.stream as Stream?)?.asBroadcastStream(); + + audioStream!.listen( + (event) {}, + cancelOnError: true, + ); + + return Response( + res.statusCode!, + body: audioStream, + context: { + "shelf.io.buffer_output": false, + }, + headers: res.headers.map, + ); + } catch (e) { + log('[PlaybackSever] Error: $e'); + return Response.internalServerError(); + } + } +} diff --git a/lib/services/server/server.dart b/lib/services/server/server.dart new file mode 100755 index 0000000..9dfec04 --- /dev/null +++ b/lib/services/server/server.dart @@ -0,0 +1,48 @@ +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' hide log; + +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart' hide Response; +import 'package:rhythm_box/services/rhythm_media.dart'; +import 'package:rhythm_box/services/server/routes/playback.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart'; +import 'package:shelf_router/shelf_router.dart'; + +class PlaybackServerProvider extends GetxController { + HttpServer? _server; + Router? _router; + + @override + void onInit() { + _initServer(); + super.onInit(); + } + + Future _initServer() async { + const pipeline = Pipeline(); + if (kDebugMode) { + pipeline.addMiddleware(logRequests()); + } + + final port = Random().nextInt(17500) + 5000; + + RhythmMedia.serverPort = port; + + _router = Router(); + _router!.get("/ping", (Request request) => Response.ok("pong")); + _router!.get( + "/stream/", + Get.find().getStreamTrackId, + ); + + _server = await serve( + pipeline.addHandler(_router!.call), + InternetAddress.anyIPv4, + port, + ); + + log('[Playback] Playback server at http://${_server!.address.host}:${_server!.port}'); + } +} diff --git a/lib/services/server/sourced_track.dart b/lib/services/server/sourced_track.dart new file mode 100755 index 0000000..136ed07 --- /dev/null +++ b/lib/services/server/sourced_track.dart @@ -0,0 +1,17 @@ +import 'package:get/get.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:rhythm_box/services/local_track.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; + +class SourcedTrackProvider extends GetxController { + Future fetch(RhythmMedia? media) async { + final track = media?.track; + if (track == null || track is LocalTrack) { + return null; + } + + final sourcedTrack = await SourcedTrack.fetchFromTrack(track: track); + + return sourcedTrack; + } +} diff --git a/lib/widgets/tracks/playlist_track_list.dart b/lib/widgets/tracks/playlist_track_list.dart index c058a13..7be2cc3 100644 --- a/lib/widgets/tracks/playlist_track_list.dart +++ b/lib/widgets/tracks/playlist_track_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -65,6 +66,10 @@ class _PlaylistTrackListState extends State { item?.artists!.map((x) => x.name!).join(', ') ?? 'Please stand by...', ), + onTap: () { + if (item == null) return; + Get.find().load([item], autoPlay: true); + }, ); }, ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ec27ef7..3665f62 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,22 @@ import FlutterMacOS import Foundation +import audio_service import audio_session +import device_info_plus import media_kit_libs_macos_audio import package_info_plus import path_provider_foundation +import shared_preferences_foundation import sqflite func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 5a55c52..db964d1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" async: dependency: transitive description: @@ -17,6 +25,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audio_service: + dependency: "direct main" + description: + name: audio_service + sha256: "9dd5ba7e77567b290c35908b1950d61485b4dfdd3a0ac398e98cfeec04651b75" + url: "https://pub.dev" + source: hosted + version: "0.18.15" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + sha256: "4cdc2127cd4562b957fb49227dc58e3303fafb09bde2573bc8241b938cf759d9" + url: "https://pub.dev" + source: hosted + version: "0.1.3" audio_session: dependency: "direct main" description: @@ -33,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" cached_network_image: dependency: "direct main" description: @@ -113,8 +153,24 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dio: + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + url: "https://pub.dev" + source: hosted + version: "10.1.2" + device_info_plus_platform_interface: dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + dio: + dependency: "direct main" description: name: dio sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" @@ -198,6 +254,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "02720226035257ad0b571c1256f43df3e1556a499f6bcb004849a0faaa0e87f0" + url: "https://pub.dev" + source: hosted + version: "1.82.6" flutter_test: dependency: "direct dev" description: flutter @@ -272,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" http_parser: dependency: transitive description: @@ -440,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" oauth2: dependency: transitive description: @@ -560,6 +640,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + puppeteer: + dependency: transitive + description: + name: puppeteer + sha256: a6752d4f09b510ae41911bfd0997f957e723d38facf320dd9ee0e5661108744a + url: "https://pub.dev" + source: hosted + version: "3.13.0" rxdart: dependency: transitive description: @@ -576,6 +672,94 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: "direct main" + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_router: + dependency: "direct main" + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" skeletonizer: dependency: "direct main" description: @@ -589,6 +773,14 @@ packages: description: flutter source: sdk version: "0.0.99" + smtc_windows: + dependency: "direct main" + description: + name: smtc_windows + sha256: "0fd64d0c6a0c8ea4ea7908d31195eadc8f6d45d5245159fc67259e9e8704100f" + url: "https://pub.dev" + source: hosted + version: "0.1.3" source_span: dependency: transitive description: @@ -685,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -710,7 +910,7 @@ packages: source: hosted version: "2.0.2" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" @@ -741,6 +941,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" win32: dependency: transitive description: @@ -749,6 +957,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.4" + win32_registry: + dependency: "direct main" + description: + name: win32_registry + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + url: "https://pub.dev" + source: hosted + version: "1.1.4" xdg_directories: dependency: transitive description: @@ -765,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" youtube_explode_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 87b983d..f7021cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,15 @@ dependencies: piped_client: ^0.1.1 flutter_broadcasts: ^0.4.0 audio_session: ^0.1.21 + shared_preferences: ^2.3.2 + audio_service: ^0.18.15 + smtc_windows: ^0.1.3 + win32_registry: ^1.1.4 + uuid: ^4.4.2 + device_info_plus: ^10.1.2 + shelf: ^1.4.1 + shelf_router: ^1.1.4 + dio: ^5.6.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index edf2630..11eb692 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST media_kit_native_event_loop + smtc_windows ) set(PLUGIN_BUNDLED_LIBRARIES)